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/index.cjs CHANGED
@@ -5,7 +5,11 @@ var addFormats = require('ajv-formats');
5
5
  var fs = require('fs');
6
6
  var promises = require('fs/promises');
7
7
  var path = require('path');
8
+ var module$1 = require('module');
9
+ var process2 = require('process');
10
+ var jiti = require('jiti');
8
11
  var jsonPtr = require('json-ptr');
12
+ var changeCase = require('change-case');
9
13
  var culori = require('culori');
10
14
  var prettier = require('prettier');
11
15
 
@@ -32,6 +36,7 @@ function _interopNamespace(e) {
32
36
  var Ajv__default = /*#__PURE__*/_interopDefault(Ajv);
33
37
  var addFormats__default = /*#__PURE__*/_interopDefault(addFormats);
34
38
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
39
+ var process2__default = /*#__PURE__*/_interopDefault(process2);
35
40
  var prettier__default = /*#__PURE__*/_interopDefault(prettier);
36
41
 
37
42
  var __defProp = Object.defineProperty;
@@ -45,7 +50,7 @@ var __export = (target, all) => {
45
50
  };
46
51
 
47
52
  // src/shared/errors/index.ts
48
- exports.DispersaError = void 0; exports.TokenReferenceError = void 0; exports.CircularReferenceError = void 0; exports.ValidationError = void 0; exports.FileOperationError = void 0; exports.ConfigurationError = void 0; exports.BasePermutationError = void 0; exports.ModifierError = void 0;
53
+ exports.DispersaError = void 0; exports.TokenReferenceError = void 0; exports.CircularReferenceError = void 0; exports.ValidationError = void 0; exports.FileOperationError = void 0; exports.ConfigurationError = void 0; exports.BasePermutationError = void 0; exports.ModifierError = void 0; exports.LintError = void 0;
49
54
  var init_errors = __esm({
50
55
  "src/shared/errors/index.ts"() {
51
56
  exports.DispersaError = class extends Error {
@@ -128,23 +133,19 @@ var init_errors = __esm({
128
133
  this.name = "ModifierError";
129
134
  }
130
135
  };
136
+ exports.LintError = class extends exports.DispersaError {
137
+ constructor(issues) {
138
+ const errorCount = issues.filter((i) => i.severity === "error").length;
139
+ const warningCount = issues.filter((i) => i.severity === "warn").length;
140
+ super(`Lint failed with ${errorCount} error(s) and ${warningCount} warning(s).`);
141
+ this.issues = issues;
142
+ this.name = "LintError";
143
+ }
144
+ };
131
145
  }
132
146
  });
133
147
 
134
148
  // src/shared/utils/token-utils.ts
135
- function formatDeprecationMessage(token, description = "", format = "bracket") {
136
- if (token.$deprecated == null || token.$deprecated === false) {
137
- return description;
138
- }
139
- const deprecationMsg = typeof token.$deprecated === "string" ? token.$deprecated : "";
140
- if (format === "comment") {
141
- const msg2 = deprecationMsg ? ` ${deprecationMsg}` : "";
142
- return `DEPRECATED${msg2}`;
143
- }
144
- const msg = deprecationMsg ? `: ${deprecationMsg}` : "";
145
- const prefix = `[DEPRECATED${msg}]`;
146
- return description ? `${prefix} ${description}` : prefix;
147
- }
148
149
  function stripInternalTokenMetadata(tokens) {
149
150
  const cleaned = {};
150
151
  for (const [name, token] of Object.entries(tokens)) {
@@ -2498,7 +2499,7 @@ var init_schemas = __esm({
2498
2499
  });
2499
2500
 
2500
2501
  // src/validation/config-schemas.ts
2501
- var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2502
+ var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, lintConfigSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2502
2503
  var init_config_schemas = __esm({
2503
2504
  "src/validation/config-schemas.ts"() {
2504
2505
  init_schemas();
@@ -2649,6 +2650,42 @@ var init_config_schemas = __esm({
2649
2650
  }
2650
2651
  }
2651
2652
  };
2653
+ lintConfigSchema = {
2654
+ $schema: "http://json-schema.org/draft-07/schema#",
2655
+ type: "object",
2656
+ properties: {
2657
+ enabled: {
2658
+ type: "boolean",
2659
+ description: "Enable linting (default: false, opt-in)"
2660
+ },
2661
+ failOnError: {
2662
+ type: "boolean",
2663
+ description: "Fail build on lint errors (default: true)"
2664
+ },
2665
+ plugins: {
2666
+ type: "object",
2667
+ description: "Plugins to load (by object or module path string)",
2668
+ additionalProperties: {
2669
+ oneOf: [{ type: "string" }, { type: "object" }]
2670
+ }
2671
+ },
2672
+ rules: {
2673
+ type: "object",
2674
+ description: "Rule configurations",
2675
+ additionalProperties: {
2676
+ oneOf: [
2677
+ { type: "string", enum: ["off", "warn", "error"] },
2678
+ {
2679
+ type: "array",
2680
+ minItems: 2,
2681
+ items: [{ type: "string", enum: ["off", "warn", "error"] }, { type: "object" }]
2682
+ }
2683
+ ]
2684
+ }
2685
+ }
2686
+ },
2687
+ additionalProperties: false
2688
+ };
2652
2689
  outputConfigSchema = {
2653
2690
  $schema: "http://json-schema.org/draft-07/schema#",
2654
2691
  type: "object",
@@ -2766,10 +2803,20 @@ var init_config_schemas = __esm({
2766
2803
  description: "Resolver configuration - file path or ResolverDocument object"
2767
2804
  },
2768
2805
  buildPath: { type: "string" },
2806
+ validation: {
2807
+ type: "object",
2808
+ properties: {
2809
+ mode: { type: "string", enum: ["error", "warn", "off"] }
2810
+ }
2811
+ },
2769
2812
  hooks: {
2770
2813
  type: "object",
2771
2814
  description: "Global build lifecycle hooks (functions, validated at runtime)",
2772
2815
  additionalProperties: true
2816
+ },
2817
+ lint: {
2818
+ ...lintConfigSchema,
2819
+ description: "Linting configuration"
2773
2820
  }
2774
2821
  },
2775
2822
  additionalProperties: false
@@ -3317,7 +3364,7 @@ function indentStr(width, level) {
3317
3364
  function buildGeneratedFileHeader() {
3318
3365
  return [
3319
3366
  "// Generated by Dispersa - do not edit manually",
3320
- "// https://github.com/timges/dispersa"
3367
+ "// https://github.com/dispersa-core/dispersa"
3321
3368
  ].join("\n");
3322
3369
  }
3323
3370
  function toSafeIdentifier(name, keywords, capitalize) {
@@ -3529,6 +3576,94 @@ var init_utils = __esm({
3529
3576
  }
3530
3577
  });
3531
3578
 
3579
+ // src/renderers/metadata.ts
3580
+ function sanitizeText(text, format) {
3581
+ switch (format) {
3582
+ case "css":
3583
+ case "tailwind":
3584
+ return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
3585
+ case "kotlin":
3586
+ return text.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
3587
+ case "js":
3588
+ case "swift":
3589
+ return text.replace(/\r?\n/g, " ").trim();
3590
+ default:
3591
+ return text.trim();
3592
+ }
3593
+ }
3594
+ function buildDeprecationText(token) {
3595
+ if (token.$deprecated == null || token.$deprecated === false) {
3596
+ return "";
3597
+ }
3598
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3599
+ return msg ? `DEPRECATED: ${msg}` : "DEPRECATED";
3600
+ }
3601
+ function buildTokenDescriptionComment(token, format) {
3602
+ if (!token.$description || token.$description === "") {
3603
+ return void 0;
3604
+ }
3605
+ const text = sanitizeText(token.$description, format);
3606
+ switch (format) {
3607
+ case "css":
3608
+ case "tailwind":
3609
+ return `/* ${text} */`;
3610
+ case "js":
3611
+ return `// ${text}`;
3612
+ case "swift":
3613
+ return `/// ${text}`;
3614
+ case "kotlin":
3615
+ return `/** ${text} */`;
3616
+ default:
3617
+ return void 0;
3618
+ }
3619
+ }
3620
+ function buildTokenDeprecationComment(token, format) {
3621
+ const text = buildDeprecationText(token);
3622
+ if (!text) {
3623
+ return void 0;
3624
+ }
3625
+ switch (format) {
3626
+ case "css":
3627
+ case "tailwind":
3628
+ return `/* ${text} */`;
3629
+ case "js":
3630
+ return `// ${text}`;
3631
+ case "swift":
3632
+ return `/// ${text}`;
3633
+ case "kotlin":
3634
+ return `/** ${text} */`;
3635
+ default:
3636
+ return void 0;
3637
+ }
3638
+ }
3639
+ function buildModifierComment(modifier, context) {
3640
+ return `/* Modifier: ${modifier}=${context} */`;
3641
+ }
3642
+ function buildSwiftDeprecationAttribute(token) {
3643
+ if (token.$deprecated == null || token.$deprecated === false) {
3644
+ return void 0;
3645
+ }
3646
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3647
+ if (msg) {
3648
+ return `@available(*, deprecated, message: "${sanitizeText(msg, "swift")}")`;
3649
+ }
3650
+ return "@available(*, deprecated)";
3651
+ }
3652
+ function buildKotlinDeprecationAnnotation(token) {
3653
+ if (token.$deprecated == null || token.$deprecated === false) {
3654
+ return void 0;
3655
+ }
3656
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3657
+ if (msg) {
3658
+ return `@Deprecated(message = "${sanitizeText(msg, "kotlin")}")`;
3659
+ }
3660
+ return "@Deprecated";
3661
+ }
3662
+ var init_metadata = __esm({
3663
+ "src/renderers/metadata.ts"() {
3664
+ }
3665
+ });
3666
+
3532
3667
  // src/renderers/bundlers/js.ts
3533
3668
  var js_exports = {};
3534
3669
  __export(js_exports, {
@@ -3633,7 +3768,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3633
3768
  }
3634
3769
  const metadata = buildMetadata(resolver);
3635
3770
  const jsBlocks = [];
3636
- for (const { tokens, modifierInputs } of bundleData) {
3771
+ for (const { tokens, modifierInputs, isBase } of bundleData) {
3637
3772
  const cleanTokens = stripInternalMetadata(tokens);
3638
3773
  const key = buildStableDashKey({
3639
3774
  modifierInputs,
@@ -3641,10 +3776,30 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3641
3776
  defaults: metadata.defaults
3642
3777
  });
3643
3778
  const camelKey = toCamelKey(key);
3779
+ const normalizedInputs = normalizeModifierInputs(modifierInputs);
3780
+ const modifierParts = [];
3781
+ for (const dim of metadata.dimensions) {
3782
+ const value = normalizedInputs[dim.toLowerCase()];
3783
+ if (value) {
3784
+ modifierParts.push(`${dim}=${value}`);
3785
+ }
3786
+ }
3644
3787
  const formattedJs = await formatTokens(cleanTokens);
3645
3788
  const tokenObject = extractObjectFromJsModule(formattedJs);
3646
3789
  const indentedObject = tokenObject.replace(/\n/g, "\n ");
3647
- jsBlocks.push(` // ${key}
3790
+ let comment;
3791
+ if (modifierParts.length > 0) {
3792
+ const modifierPart = modifierParts[0];
3793
+ const eqIndex = modifierPart.indexOf("=");
3794
+ const modifier = modifierPart.slice(0, eqIndex);
3795
+ const context = modifierPart.slice(eqIndex + 1);
3796
+ comment = buildModifierComment(modifier, context);
3797
+ } else if (isBase) {
3798
+ comment = "// Base permutation";
3799
+ } else {
3800
+ comment = `// ${key}`;
3801
+ }
3802
+ jsBlocks.push(` ${comment}
3648
3803
  ${JSON.stringify(camelKey)}: ${indentedObject}`);
3649
3804
  }
3650
3805
  return assembleJsBundle(metadata, jsBlocks, options?.generateHelper ?? false);
@@ -3652,6 +3807,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3652
3807
  var init_js = __esm({
3653
3808
  "src/renderers/bundlers/js.ts"() {
3654
3809
  init_errors();
3810
+ init_metadata();
3655
3811
  init_utils();
3656
3812
  }
3657
3813
  });
@@ -4021,7 +4177,8 @@ var BuildOrchestrator = class {
4021
4177
  resolver,
4022
4178
  config.transforms,
4023
4179
  config.preprocessors,
4024
- config.filters
4180
+ config.filters,
4181
+ config.lint
4025
4182
  );
4026
4183
  return this.executeBuild(buildPath, config, permutations2, resolver);
4027
4184
  }
@@ -4032,7 +4189,8 @@ var BuildOrchestrator = class {
4032
4189
  modifierInputs,
4033
4190
  config.transforms,
4034
4191
  config.preprocessors,
4035
- config.filters
4192
+ config.filters,
4193
+ config.lint
4036
4194
  );
4037
4195
  return { tokens, modifierInputs: resolvedInputs };
4038
4196
  })
@@ -4293,7 +4451,358 @@ var OutputProcessor = class {
4293
4451
 
4294
4452
  // src/build/pipeline/token-pipeline.ts
4295
4453
  init_resolver_loader();
4296
- init_validation_handler();
4454
+
4455
+ // src/lint/plugin-loader.ts
4456
+ init_errors();
4457
+ var PluginLoader = class {
4458
+ cwd;
4459
+ jiti = null;
4460
+ cache = /* @__PURE__ */ new Map();
4461
+ constructor(options = {}) {
4462
+ this.cwd = options.cwd ?? process2__default.default.cwd();
4463
+ }
4464
+ /**
4465
+ * Load a plugin from an inline object or module path
4466
+ *
4467
+ * @param source - Plugin object or module path string
4468
+ * @returns Loaded plugin
4469
+ * @throws {ConfigurationError} If plugin cannot be loaded or is invalid
4470
+ */
4471
+ async load(source) {
4472
+ if (this.isPluginObject(source)) {
4473
+ this.validatePlugin(source);
4474
+ return source;
4475
+ }
4476
+ const modulePath = source;
4477
+ const cached = this.cache.get(modulePath);
4478
+ if (cached) {
4479
+ return cached;
4480
+ }
4481
+ const plugin = await this.loadFromModule(modulePath);
4482
+ this.validatePlugin(plugin);
4483
+ this.cache.set(modulePath, plugin);
4484
+ return plugin;
4485
+ }
4486
+ /**
4487
+ * Load multiple plugins
4488
+ *
4489
+ * @param plugins - Record of namespace to plugin source
4490
+ * @returns Record of namespace to loaded plugin
4491
+ */
4492
+ async loadAll(plugins) {
4493
+ const entries = Object.entries(plugins);
4494
+ const loaded = await Promise.all(
4495
+ entries.map(async ([namespace, source]) => [namespace, await this.load(source)])
4496
+ );
4497
+ return Object.fromEntries(loaded);
4498
+ }
4499
+ /**
4500
+ * Check if source is an inline plugin object
4501
+ */
4502
+ isPluginObject(source) {
4503
+ return typeof source !== "string";
4504
+ }
4505
+ /**
4506
+ * Load a plugin from a module path
4507
+ */
4508
+ async loadFromModule(modulePath) {
4509
+ const resolvedPath = path.isAbsolute(modulePath) ? modulePath : path.resolve(this.cwd, modulePath);
4510
+ const isFilePath = modulePath.startsWith("./") || modulePath.startsWith("../") || path.isAbsolute(modulePath);
4511
+ try {
4512
+ if (isFilePath) {
4513
+ return await this.loadFromFile(resolvedPath);
4514
+ }
4515
+ return await this.loadFromPackage(modulePath);
4516
+ } catch (error) {
4517
+ const message = error instanceof Error ? error.message : String(error);
4518
+ throw new exports.ConfigurationError(`Failed to load lint plugin '${modulePath}': ${message}`);
4519
+ }
4520
+ }
4521
+ /**
4522
+ * Load a plugin from a file path using jiti (supports TypeScript)
4523
+ */
4524
+ async loadFromFile(filePath) {
4525
+ this.jiti ??= jiti.createJiti(this.cwd, {
4526
+ interopDefault: true
4527
+ });
4528
+ const loaded = await this.jiti(filePath);
4529
+ const plugin = this.extractPlugin(loaded);
4530
+ if (!plugin) {
4531
+ throw new exports.ConfigurationError(`Plugin file '${filePath}' does not export a valid LintPlugin`);
4532
+ }
4533
+ return plugin;
4534
+ }
4535
+ /**
4536
+ * Load a plugin from a package name
4537
+ */
4538
+ async loadFromPackage(packageName) {
4539
+ const require2 = module$1.createRequire(this.cwd);
4540
+ let resolvedPath;
4541
+ try {
4542
+ resolvedPath = require2.resolve(packageName, { paths: [this.cwd] });
4543
+ } catch {
4544
+ try {
4545
+ resolvedPath = require2.resolve(packageName);
4546
+ } catch {
4547
+ throw new exports.ConfigurationError(
4548
+ `Cannot find package '${packageName}'. Make sure it is installed.`
4549
+ );
4550
+ }
4551
+ }
4552
+ this.jiti ??= jiti.createJiti(this.cwd, {
4553
+ interopDefault: true
4554
+ });
4555
+ const loaded = await this.jiti(resolvedPath);
4556
+ const plugin = this.extractPlugin(loaded);
4557
+ if (!plugin) {
4558
+ throw new exports.ConfigurationError(`Package '${packageName}' does not export a valid LintPlugin`);
4559
+ }
4560
+ return plugin;
4561
+ }
4562
+ /**
4563
+ * Extract plugin from loaded module
4564
+ *
4565
+ * Supports multiple export patterns:
4566
+ * - export default plugin
4567
+ * - export const plugin = {...}
4568
+ * - module.exports = plugin (CJS)
4569
+ */
4570
+ extractPlugin(loaded) {
4571
+ if (!loaded || typeof loaded !== "object") {
4572
+ return null;
4573
+ }
4574
+ const module = loaded;
4575
+ if (module.default && this.isValidPluginStructure(module.default)) {
4576
+ return module.default;
4577
+ }
4578
+ if (module.plugin && this.isValidPluginStructure(module.plugin)) {
4579
+ return module.plugin;
4580
+ }
4581
+ if (this.isValidPluginStructure(loaded)) {
4582
+ return loaded;
4583
+ }
4584
+ return null;
4585
+ }
4586
+ /**
4587
+ * Check if object has required plugin structure
4588
+ */
4589
+ isValidPluginStructure(obj) {
4590
+ if (!obj || typeof obj !== "object") {
4591
+ return false;
4592
+ }
4593
+ const plugin = obj;
4594
+ const rules = plugin.rules;
4595
+ if (plugin.meta === void 0 || typeof plugin.meta !== "object" || !plugin.meta.name || rules === void 0 || Object.keys(rules).length === 0) {
4596
+ return false;
4597
+ }
4598
+ return true;
4599
+ }
4600
+ /**
4601
+ * Validate a loaded plugin
4602
+ */
4603
+ validatePlugin(plugin) {
4604
+ if (!plugin.meta) {
4605
+ throw new exports.ConfigurationError("Lint plugin must have a meta property with name");
4606
+ }
4607
+ if (!plugin.meta.name) {
4608
+ throw new exports.ConfigurationError("Lint plugin meta.name is required");
4609
+ }
4610
+ if (!plugin.rules || typeof plugin.rules !== "object" || Object.keys(plugin.rules).length === 0) {
4611
+ throw new exports.ConfigurationError(
4612
+ `Lint plugin '${plugin.meta.name}' must have a non-empty rules object`
4613
+ );
4614
+ }
4615
+ for (const [ruleName, rule] of Object.entries(plugin.rules)) {
4616
+ if (!rule.meta) {
4617
+ throw new exports.ConfigurationError(
4618
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta property`
4619
+ );
4620
+ }
4621
+ if (!rule.meta.messages || typeof rule.meta.messages !== "object") {
4622
+ throw new exports.ConfigurationError(
4623
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta.messages`
4624
+ );
4625
+ }
4626
+ if (typeof rule.create !== "function") {
4627
+ throw new exports.ConfigurationError(
4628
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing create function`
4629
+ );
4630
+ }
4631
+ }
4632
+ }
4633
+ /**
4634
+ * Clear the plugin cache
4635
+ */
4636
+ clearCache() {
4637
+ this.cache.clear();
4638
+ }
4639
+ };
4640
+
4641
+ // src/lint/lint-runner.ts
4642
+ var LintRunner = class {
4643
+ config;
4644
+ pluginLoader;
4645
+ resolvedConfig = null;
4646
+ warn;
4647
+ constructor(config) {
4648
+ this.config = config;
4649
+ this.pluginLoader = new PluginLoader();
4650
+ this.warn = config.onWarn ?? console.warn;
4651
+ }
4652
+ /**
4653
+ * Run all configured rules against the provided tokens
4654
+ *
4655
+ * Rules are executed in parallel for performance. Issues are collected
4656
+ * and returned with counts by severity.
4657
+ *
4658
+ * @param tokens - Resolved tokens to lint
4659
+ * @returns Lint result with issues and counts
4660
+ */
4661
+ async run(tokens) {
4662
+ this.resolvedConfig ??= await this.resolveConfig();
4663
+ const { rules, plugins } = this.resolvedConfig;
4664
+ const issues = [];
4665
+ if (Object.keys(rules).length === 0) {
4666
+ return { issues: [], errorCount: 0, warningCount: 0 };
4667
+ }
4668
+ const rulePromises = Object.entries(rules).map(async ([ruleId, ruleConfig]) => {
4669
+ const { severity, options } = ruleConfig;
4670
+ const rule = this.resolveRule(ruleId, plugins);
4671
+ if (!rule) {
4672
+ this.warn(`[lint] Unknown rule '${ruleId}' - no plugin provides this rule`);
4673
+ return [];
4674
+ }
4675
+ const reports = [];
4676
+ const applicableTokens = this.filterTokensByAppliesTo(tokens, rule.meta.appliesTo);
4677
+ const mergedOptions = rule.defaultOptions ? { ...rule.defaultOptions, ...options } : options;
4678
+ const context = {
4679
+ id: ruleId,
4680
+ options: mergedOptions,
4681
+ tokens: applicableTokens,
4682
+ report: (descriptor) => {
4683
+ reports.push(descriptor);
4684
+ }
4685
+ };
4686
+ try {
4687
+ await rule.create(context);
4688
+ } catch (error) {
4689
+ const message = error instanceof Error ? error.message : String(error);
4690
+ return [
4691
+ {
4692
+ ruleId: "lint/rule-error",
4693
+ severity: "error",
4694
+ message: `Rule '${ruleId}' failed: ${message}`,
4695
+ tokenName: "(rule execution)",
4696
+ tokenPath: []
4697
+ }
4698
+ ];
4699
+ }
4700
+ return reports.map((report) => {
4701
+ const messageTemplate = rule.meta.messages[report.messageId];
4702
+ const message = messageTemplate ? this.interpolateMessage(messageTemplate, report.data) : report.messageId;
4703
+ return {
4704
+ ruleId,
4705
+ severity,
4706
+ message,
4707
+ tokenName: report.token.name,
4708
+ tokenPath: report.token.path
4709
+ };
4710
+ });
4711
+ });
4712
+ const allIssues = await Promise.all(rulePromises);
4713
+ issues.push(...allIssues.flat());
4714
+ const errorCount = issues.filter((i) => i.severity === "error").length;
4715
+ const warningCount = issues.filter((i) => i.severity === "warn").length;
4716
+ return { issues, errorCount, warningCount };
4717
+ }
4718
+ /**
4719
+ * Resolve configuration: load plugins, parse rule configs
4720
+ */
4721
+ async resolveConfig() {
4722
+ const { plugins: pluginSources = {}, rules: ruleConfigs = {} } = this.config;
4723
+ const plugins = await this.pluginLoader.loadAll(pluginSources);
4724
+ const rules = {};
4725
+ for (const [ruleId, config] of Object.entries(ruleConfigs)) {
4726
+ const resolved = this.resolveRuleConfig(config);
4727
+ if (resolved) {
4728
+ rules[ruleId] = resolved;
4729
+ }
4730
+ }
4731
+ return {
4732
+ enabled: true,
4733
+ failOnError: this.config.failOnError ?? true,
4734
+ plugins,
4735
+ rules
4736
+ };
4737
+ }
4738
+ filterTokensByAppliesTo(tokens, appliesTo) {
4739
+ if (!appliesTo || appliesTo === "all") {
4740
+ return tokens;
4741
+ }
4742
+ const filtered = {};
4743
+ for (const [name, token] of Object.entries(tokens)) {
4744
+ if (token.$type && appliesTo.includes(token.$type)) {
4745
+ filtered[name] = token;
4746
+ }
4747
+ }
4748
+ return filtered;
4749
+ }
4750
+ /**
4751
+ * Parse rule configuration into resolved format
4752
+ */
4753
+ resolveRuleConfig(config) {
4754
+ if (typeof config === "string") {
4755
+ if (config === "off") {
4756
+ return null;
4757
+ }
4758
+ return { severity: config, options: {} };
4759
+ }
4760
+ const [severity, options = {}] = config;
4761
+ if (severity === "off") {
4762
+ return null;
4763
+ }
4764
+ return { severity, options };
4765
+ }
4766
+ /**
4767
+ * Resolve a rule from plugins by rule ID
4768
+ *
4769
+ * Rule IDs are formatted as 'namespace/rule-name'
4770
+ */
4771
+ resolveRule(ruleId, plugins) {
4772
+ const separatorIndex = ruleId.indexOf("/");
4773
+ if (separatorIndex === -1) {
4774
+ return null;
4775
+ }
4776
+ const namespace = ruleId.slice(0, separatorIndex);
4777
+ const ruleName = ruleId.slice(separatorIndex + 1);
4778
+ const plugin = plugins[namespace];
4779
+ if (!plugin) {
4780
+ return null;
4781
+ }
4782
+ return plugin.rules[ruleName] ?? null;
4783
+ }
4784
+ /**
4785
+ * Interpolate message template with data
4786
+ *
4787
+ * Replaces {{key}} placeholders with values from data
4788
+ */
4789
+ interpolateMessage(template, data) {
4790
+ if (!data) {
4791
+ return template;
4792
+ }
4793
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
4794
+ const value = data[key];
4795
+ return value !== void 0 ? String(value) : `{{${key}}}`;
4796
+ });
4797
+ }
4798
+ /**
4799
+ * Clear the plugin cache
4800
+ */
4801
+ clearCache() {
4802
+ this.pluginLoader.clearCache();
4803
+ this.resolvedConfig = null;
4804
+ }
4805
+ };
4297
4806
 
4298
4807
  // src/shared/constants.ts
4299
4808
  var DEFAULT_MAX_ALIAS_DEPTH = 10;
@@ -5488,6 +5997,10 @@ var ResolutionEngine = class {
5488
5997
  return typeof obj === "object" && obj !== null && "contexts" in obj && typeof obj.contexts === "object";
5489
5998
  }
5490
5999
  };
6000
+
6001
+ // src/build/pipeline/token-pipeline.ts
6002
+ init_errors();
6003
+ init_validation_handler();
5491
6004
  init_errors();
5492
6005
 
5493
6006
  // src/shared/utils/path-utils.ts
@@ -5980,6 +6493,8 @@ var TokenPipeline = class {
5980
6493
  resolverLoader;
5981
6494
  tokenParser;
5982
6495
  aliasResolver;
6496
+ lintRunner = null;
6497
+ lintConfigCache = null;
5983
6498
  constructor(options = {}) {
5984
6499
  this.options = options;
5985
6500
  this.validationHandler = new ValidationHandler(options.validation);
@@ -5999,8 +6514,9 @@ var TokenPipeline = class {
5999
6514
  * 6. Parse and flatten token structure
6000
6515
  * 7. Resolve alias references
6001
6516
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
6002
- * 9. Apply filters (if provided) — runs first to remove tokens before transforms
6003
- * 10. Apply transforms (if provided) — runs on the already-filtered token set
6517
+ * 9. Run lint rules (if enabled)
6518
+ * 10. Apply filters (if provided) — runs first to remove tokens before transforms
6519
+ * 11. Apply transforms (if provided) — runs on the already-filtered token set
6004
6520
  *
6005
6521
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
6006
6522
  *
@@ -6009,9 +6525,11 @@ var TokenPipeline = class {
6009
6525
  * @param transformList - Optional transforms to apply
6010
6526
  * @param preprocessorList - Optional preprocessors to apply
6011
6527
  * @param filterList - Optional filters to apply before transforms
6012
- * @returns Final tokens and resolution engine
6528
+ * @param lintConfig - Optional lint configuration for this run
6529
+ * @returns Final tokens, resolution engine, and lint result
6013
6530
  */
6014
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6531
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6532
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6015
6533
  const stage1 = await this.loadResolver(resolver);
6016
6534
  const engine = this.createEngine(stage1);
6017
6535
  const result = await this.runPipelineStages(
@@ -6019,29 +6537,32 @@ var TokenPipeline = class {
6019
6537
  modifierInputs,
6020
6538
  preprocessorList,
6021
6539
  filterList,
6022
- transformList
6540
+ transformList,
6541
+ effectiveLintConfig
6023
6542
  );
6024
6543
  return {
6025
6544
  tokens: result.tokens,
6026
6545
  resolutionEngine: result.resolutionEngine,
6027
- modifierInputs: result.modifierInputs
6546
+ modifierInputs: result.modifierInputs,
6547
+ lintResult: result.lintResult
6028
6548
  };
6029
6549
  }
6030
6550
  /**
6031
- * Run pipeline stages 3-9 on a pre-created engine
6551
+ * Run pipeline stages 3-11 on a pre-created engine
6032
6552
  *
6033
6553
  * Shared by both `resolve()` (single permutation) and
6034
6554
  * `resolveAllPermutations()` (parallel permutations) to keep the
6035
6555
  * stage sequence defined in exactly one place.
6036
6556
  */
6037
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6557
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6038
6558
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6039
6559
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6040
6560
  const refResolved = await this.resolveReferences(preprocessed);
6041
6561
  const flattened = this.flattenTokens(refResolved);
6042
6562
  const aliasResolved = this.resolveAliases(flattened);
6043
6563
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6044
- const filtered = this.applyFilterStage(rootStripped, filterList);
6564
+ const linted = await this.runLintStage(rootStripped, lintConfig);
6565
+ const filtered = this.applyFilterStage(linted, filterList);
6045
6566
  return this.applyTransformStage(filtered, transformList);
6046
6567
  }
6047
6568
  /**
@@ -6154,6 +6675,47 @@ var TokenPipeline = class {
6154
6675
  }
6155
6676
  return { ...stage, aliasResolvedTokens: result };
6156
6677
  }
6678
+ /**
6679
+ * Stage 9: Run lint rules against tokens
6680
+ *
6681
+ * Linting runs after alias resolution and $root stripping but before
6682
+ * filters and transforms. This ensures rules see the full token set
6683
+ * with resolved values.
6684
+ */
6685
+ async runLintStage(stage, lintConfig) {
6686
+ if (!lintConfig?.enabled) {
6687
+ return stage;
6688
+ }
6689
+ if (!this.lintRunner || !this.isLintConfigEqual(this.lintConfigCache, lintConfig)) {
6690
+ this.lintRunner = new LintRunner({
6691
+ plugins: lintConfig.plugins,
6692
+ rules: lintConfig.rules,
6693
+ onWarn: (msg) => this.validationHandler.warn(msg)
6694
+ });
6695
+ this.lintConfigCache = lintConfig;
6696
+ }
6697
+ const lintResult = await this.lintRunner.run(stage.aliasResolvedTokens);
6698
+ if (lintResult.errorCount > 0 && lintConfig.failOnError !== false) {
6699
+ throw new exports.LintError(lintResult.issues);
6700
+ }
6701
+ for (const issue of lintResult.issues.filter((i) => i.severity === "warn")) {
6702
+ this.validationHandler.warn(
6703
+ `[${issue.ruleId}] ${issue.message} (token: ${issue.tokenName})`
6704
+ );
6705
+ }
6706
+ return { ...stage, lintResult };
6707
+ }
6708
+ isLintConfigEqual(a, b) {
6709
+ if (!a || !b) return false;
6710
+ if (a.enabled !== b.enabled) return false;
6711
+ if (a.failOnError !== b.failOnError) return false;
6712
+ if (a.plugins !== b.plugins) return false;
6713
+ if (a.rules !== b.rules) return false;
6714
+ return true;
6715
+ }
6716
+ /**
6717
+ * Stage 10: Apply filters to the linted token set
6718
+ */
6157
6719
  applyFilterStage(stage, filterList) {
6158
6720
  let tokens = stage.aliasResolvedTokens;
6159
6721
  if (filterList !== void 0 && filterList.length > 0) {
@@ -6162,7 +6724,7 @@ var TokenPipeline = class {
6162
6724
  return { ...stage, aliasResolvedTokens: tokens };
6163
6725
  }
6164
6726
  /**
6165
- * Stage 9: Apply transforms to the filtered token set
6727
+ * Stage 11: Apply transforms to the filtered token set
6166
6728
  */
6167
6729
  applyTransformStage(stage, transformList) {
6168
6730
  let tokens = stage.aliasResolvedTokens;
@@ -6196,8 +6758,10 @@ var TokenPipeline = class {
6196
6758
  * @param transformList - Optional transforms to apply
6197
6759
  * @param preprocessorList - Optional preprocessors to apply
6198
6760
  * @param filterList - Optional filters to apply before transforms
6761
+ * @param lintConfig - Optional lint configuration for this run
6199
6762
  */
6200
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6763
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6764
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6201
6765
  const stage1 = await this.loadResolver(resolver);
6202
6766
  const discoveryEngine = this.createEngine(stage1);
6203
6767
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6210,11 +6774,13 @@ var TokenPipeline = class {
6210
6774
  modifierInputs,
6211
6775
  preprocessorList,
6212
6776
  filterList,
6213
- transformList
6777
+ transformList,
6778
+ effectiveLintConfig
6214
6779
  );
6215
6780
  return {
6216
6781
  tokens: result.tokens,
6217
- modifierInputs: result.modifierInputs
6782
+ modifierInputs: result.modifierInputs,
6783
+ lintResult: result.lintResult
6218
6784
  };
6219
6785
  })
6220
6786
  );
@@ -6235,286 +6801,106 @@ var TokenPipeline = class {
6235
6801
  init_errors();
6236
6802
  init_token_utils();
6237
6803
  init_validator();
6238
- var Dispersa = class {
6239
- validator;
6240
- pipeline;
6241
- outputProcessor;
6242
- orchestrator;
6243
- options;
6244
- /**
6245
- * Creates a new Dispersa instance
6246
- *
6247
- * @param options - Configuration options (optional, can be empty object)
6248
- * @param options.resolver - Default resolver (file path or inline object, optional if provided at build time)
6249
- * @param options.buildPath - Default output directory for generated files (omit for in-memory mode)
6250
- * @throws {ConfigurationError} If options are invalid and validation is enabled
6251
- */
6252
- constructor(options = {}) {
6253
- this.options = options;
6254
- this.validator = new SchemaValidator();
6255
- const errors = this.validator.validateDispersaOptions(options);
6256
- if (errors.length > 0) {
6257
- throw new exports.ConfigurationError(
6258
- `Invalid Dispersa options: ${this.validator.getErrorMessage(errors)}`
6259
- );
6260
- }
6261
- this.pipeline = new TokenPipeline({ validation: options.validation });
6262
- this.outputProcessor = new OutputProcessor();
6263
- this.orchestrator = new BuildOrchestrator(this.pipeline, this.outputProcessor);
6264
- }
6265
- /**
6266
- * Builds design tokens from a resolver configuration
6267
- *
6268
- * Processes tokens through the resolution pipeline, applies preprocessors,
6269
- * transforms, and filters, then generates output files in multiple formats
6270
- * for specified outputs.
6271
- *
6272
- * **Runtime Validation:**
6273
- * This method validates the build configuration
6274
- * and all output configurations before processing, throwing helpful errors for
6275
- * invalid inputs.
6276
- *
6277
- * **Permutation Handling:**
6278
- * - If `config.permutations` is provided and non-empty, builds those specific permutations
6279
- * - If `config.permutations` is undefined or empty, auto-discovers all permutations from resolver
6280
- *
6281
- * @param config - Build configuration
6282
- * @param config.resolver - Resolver configuration (file path or inline object, optional if set in constructor)
6283
- * @param config.outputs - Array of output configurations (renderers, transforms, filters)
6284
- * @param config.buildPath - Output directory for generated files (omit for in-memory mode, optional if set in constructor)
6285
- * @param config.transforms - Global transforms to apply to all tokens
6286
- * @param config.preprocessors - Global preprocessors to apply before parsing
6287
- * @param config.permutations - Array of modifier inputs for generating variations
6288
- * @returns Build result with success status and generated output files
6289
- * @throws {ConfigurationError} If configuration is invalid
6290
- * @throws {FileOperationError} If file operations fail
6291
- *
6292
- * @example Basic build
6293
- * ```typescript
6294
- * const dispersa = new Dispersa({
6295
- * resolver: './tokens.resolver.json',
6296
- * buildPath: './output'
6297
- * })
6298
- *
6299
- * const result = await dispersa.build({
6300
- * outputs: [
6301
- * css({
6302
- * name: 'css',
6303
- * file: 'tokens.css',
6304
- * preset: 'bundle',
6305
- * selector: ':root',
6306
- * transforms: [nameKebabCase()]
6307
- * })
6308
- * ]
6309
- * })
6310
- * ```
6311
- *
6312
- * @example With filters and multiple presets
6313
- * ```typescript
6314
- * const result = await dispersa.build({
6315
- * resolver: './tokens.resolver.json',
6316
- * outputs: [
6317
- * css({
6318
- * name: 'css',
6319
- * file: 'tokens.css',
6320
- * preset: 'bundle',
6321
- * selector: ':root', // All themes in one file
6322
- * transforms: [nameKebabCase()]
6323
- * }),
6324
- * json({
6325
- * name: 'json',
6326
- * file: 'tokens-{theme}.json', // Separate file per theme
6327
- * preset: 'standalone',
6328
- * structure: 'flat'
6329
- * })
6330
- * ],
6331
- * buildPath: './output',
6332
- * permutations: [
6333
- * { theme: 'light' },
6334
- * { theme: 'dark' }
6335
- * ]
6336
- * })
6337
- * ```
6338
- */
6339
- async build(config) {
6340
- try {
6341
- return await this.buildOrThrow(config);
6342
- } catch (error) {
6343
- return {
6344
- success: false,
6345
- outputs: [],
6346
- errors: [toBuildError(error)]
6347
- };
6348
- }
6349
- }
6350
- /**
6351
- * Builds design tokens and throws on any failure.
6352
- *
6353
- * Unlike {@link build}, which catches errors and returns them inside
6354
- * `BuildResult.errors`, this method propagates the first error as an
6355
- * exception. Use it when you want fail-fast behavior in CLI or CI workflows.
6356
- *
6357
- * @param config - Build configuration specifying resolver, outputs, transforms, etc.
6358
- * @returns A successful `BuildResult` (never contains errors)
6359
- * @throws {ConfigurationError} When the build config or resolver is invalid
6360
- * @throws {DispersaError} When token resolution, transforms, or rendering fails
6361
- *
6362
- * @example
6363
- * ```typescript
6364
- * try {
6365
- * const result = await dispersa.buildOrThrow({
6366
- * resolver: './tokens.resolver.json',
6367
- * outputs: [css({ name: 'css', file: 'tokens.css' })],
6368
- * buildPath: './output',
6369
- * })
6370
- * } catch (error) {
6371
- * process.exit(1)
6372
- * }
6373
- * ```
6374
- */
6375
- async buildOrThrow(config) {
6376
- const configErrors = this.validator.validateBuildConfig(config);
6377
- if (configErrors.length > 0) {
6378
- throw new exports.ConfigurationError(
6379
- `Invalid build configuration: ${this.validator.getErrorMessage(configErrors)}`
6380
- );
6381
- }
6382
- for (const output of config.outputs) {
6383
- const outputErrors = this.validator.validateOutputConfig(output);
6384
- if (outputErrors.length > 0) {
6385
- throw new exports.ConfigurationError(
6386
- `Invalid output '${output.name}': ${this.validator.getErrorMessage(outputErrors)}`
6387
- );
6388
- }
6389
- }
6390
- const { resolver, buildPath } = this.resolveConfig(config);
6391
- return this.orchestrator.build(resolver, buildPath, config);
6804
+ function createValidator() {
6805
+ return new SchemaValidator();
6806
+ }
6807
+ function createPipeline(options) {
6808
+ return new TokenPipeline({ validation: options?.validation });
6809
+ }
6810
+ function createOutputProcessor() {
6811
+ return new OutputProcessor();
6812
+ }
6813
+ function createOrchestrator(pipeline, outputProcessor) {
6814
+ return new BuildOrchestrator(pipeline, outputProcessor);
6815
+ }
6816
+ function resolveConfig(config, options) {
6817
+ const resolver = config.resolver ?? options?.resolver;
6818
+ const buildPath = config.buildPath ?? options?.buildPath ?? "";
6819
+ if (!resolver) {
6820
+ throw new exports.ConfigurationError("resolver is required in build config");
6392
6821
  }
6393
- /**
6394
- * Builds tokens for a single permutation with all configured outputs
6395
- *
6396
- * Convenience wrapper around `build()` that forces a single permutation.
6397
- * This means it has the same runtime validation and error semantics as `build()`:
6398
- * it returns a `BuildResult` object rather than throwing (use `buildOrThrow()` if you
6399
- * want fail-fast behavior).
6400
- *
6401
- * @param config - Build configuration
6402
- * @param modifierInputs - Modifier values (e.g., `{ theme: 'dark' }`)
6403
- * @returns Build result (success, outputs, optional errors)
6404
- */
6405
- async buildPermutation(config, modifierInputs = {}) {
6406
- return await this.build({
6407
- ...config,
6408
- permutations: [modifierInputs]
6409
- });
6822
+ return { resolver, buildPath };
6823
+ }
6824
+ function validateBuildConfig(validator, config) {
6825
+ const configErrors = validator.validateBuildConfig(config);
6826
+ if (configErrors.length > 0) {
6827
+ throw new exports.ConfigurationError(
6828
+ `Invalid build configuration: ${validator.getErrorMessage(configErrors)}`
6829
+ );
6410
6830
  }
6411
- /**
6412
- * Resolve configuration with constructor defaults
6413
- */
6414
- resolveConfig(config) {
6415
- const resolver = config.resolver ?? this.options.resolver;
6416
- const buildPath = config.buildPath ?? this.options.buildPath ?? "";
6417
- if (!resolver) {
6831
+ for (const output of config.outputs) {
6832
+ const outputErrors = validator.validateOutputConfig(output);
6833
+ if (outputErrors.length > 0) {
6418
6834
  throw new exports.ConfigurationError(
6419
- "resolver must be provided either in constructor options or build config"
6835
+ `Invalid output '${output.name}': ${validator.getErrorMessage(outputErrors)}`
6420
6836
  );
6421
6837
  }
6422
- return { resolver, buildPath };
6423
6838
  }
6424
- /**
6425
- * Resolves tokens for a specific permutation without generating output files
6426
- *
6427
- * Useful for programmatic access to resolved token values, testing,
6428
- * or implementing custom output logic. Returns fully resolved tokens
6429
- * with all references and aliases resolved.
6430
- *
6431
- * @param resolver - Resolver configuration (file path or inline object)
6432
- * @param modifierInputs - Modifier values for this permutation (e.g., `{ theme: 'dark' }`)
6433
- * @returns Object mapping token names to resolved token objects
6434
- * @throws {FileOperationError} If resolver file cannot be read
6435
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6436
- *
6437
- * @example
6438
- * ```typescript
6439
- * const tokens = await dispersa.resolveTokens(
6440
- * './tokens.resolver.json',
6441
- * { theme: 'dark' }
6442
- * )
6443
- *
6444
- * console.log(tokens['color.background'].$value) // '#1a1a1a'
6445
- * ```
6446
- */
6447
- async resolveTokens(resolver, modifierInputs = {}) {
6448
- const { tokens } = await this.pipeline.resolve(resolver, modifierInputs);
6449
- return stripInternalTokenMetadata(tokens);
6450
- }
6451
- /**
6452
- * Resolves tokens for all permutations defined in the resolver
6453
- *
6454
- * Auto-discovers all possible permutations from the resolver's modifier
6455
- * definitions and resolves tokens for each one. Useful for generating
6456
- * comprehensive token sets or validating all theme variations.
6457
- *
6458
- * @param resolver - Resolver configuration (file path or inline object)
6459
- * @returns Array of resolved token sets with their modifier inputs
6460
- * @throws {FileOperationError} If resolver file cannot be read
6461
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6462
- *
6463
- * @example
6464
- * ```typescript
6465
- * const permutations = await dispersa.resolveAllPermutations(
6466
- * './tokens.resolver.json'
6467
- * )
6468
- *
6469
- * permutations.forEach(({ tokens, modifierInputs }) => {
6470
- * console.log(`Theme: ${modifierInputs.theme}`)
6471
- * console.log(`Tokens: ${Object.keys(tokens).length}`)
6472
- * })
6473
- * ```
6474
- */
6475
- async resolveAllPermutations(resolver) {
6476
- const permutations = await this.pipeline.resolveAllPermutations(resolver);
6477
- return permutations.map(({ tokens, modifierInputs }) => ({
6478
- tokens: stripInternalTokenMetadata(tokens),
6479
- modifierInputs
6480
- }));
6839
+ }
6840
+ async function resolvePipeline(pipeline, resolver, modifierInputs) {
6841
+ const { tokens } = await pipeline.resolve(resolver, modifierInputs);
6842
+ return stripInternalTokenMetadata(tokens);
6843
+ }
6844
+ async function build(config) {
6845
+ try {
6846
+ return await buildOrThrow(config);
6847
+ } catch (error) {
6848
+ return {
6849
+ success: false,
6850
+ outputs: [],
6851
+ errors: [toBuildError(error)]
6852
+ };
6481
6853
  }
6482
- /**
6483
- * Generates TypeScript type definitions from resolved tokens
6484
- *
6485
- * Creates a `.d.ts` file with type-safe token definitions including:
6486
- * - Token name union type for autocomplete
6487
- * - Token value types
6488
- * - Nested structure types matching token organization
6489
- *
6490
- * @param tokens - Resolved tokens object
6491
- * @param fileName - Path for the generated `.d.ts` file
6492
- * @param options - Generation options
6493
- * @param options.moduleName - Name for the exported types (default: 'Tokens')
6494
- * @returns Promise that resolves when file is written
6495
- * @throws {FileOperationError} If file cannot be written
6496
- *
6497
- * @example
6498
- * ```typescript
6499
- * const tokens = await dispersa.resolveTokens('./tokens.resolver.json')
6500
- * await dispersa.generateTypes(tokens, './src/tokens.d.ts', {
6501
- * moduleName: 'DesignTokens'
6502
- * })
6503
- *
6504
- * // Generated types can be used like:
6505
- * // const tokenName: TokenName = 'color.background'
6506
- * // const tokens: DesignTokens = { ... }
6507
- * ```
6508
- */
6509
- async generateTypes(tokens, fileName, options) {
6510
- const typeWriter = new TypeWriter();
6511
- await typeWriter.write(tokens, {
6512
- fileName,
6513
- moduleName: options?.moduleName ?? "Tokens",
6514
- includeValues: true
6515
- });
6854
+ }
6855
+ async function buildOrThrow(config) {
6856
+ const validator = createValidator();
6857
+ validateBuildConfig(validator, config);
6858
+ const { resolver, buildPath } = resolveConfig(config);
6859
+ const pipeline = createPipeline({ validation: config.validation });
6860
+ const outputProcessor = createOutputProcessor();
6861
+ const orchestrator = createOrchestrator(pipeline, outputProcessor);
6862
+ return orchestrator.build(resolver, buildPath, config);
6863
+ }
6864
+ async function buildPermutation(config, modifierInputs = {}) {
6865
+ return build({
6866
+ ...config,
6867
+ permutations: [modifierInputs]
6868
+ });
6869
+ }
6870
+ async function resolveTokens(resolver, modifierInputs = {}, validation) {
6871
+ const pipeline = createPipeline({ validation });
6872
+ return resolvePipeline(pipeline, resolver, modifierInputs);
6873
+ }
6874
+ async function lint(options) {
6875
+ const { resolver, modifierInputs = {}, validation, ...lintConfig } = options;
6876
+ const pipeline = createPipeline({ validation });
6877
+ const tokens = await resolvePipeline(pipeline, resolver, modifierInputs);
6878
+ const runner = new LintRunner({
6879
+ ...lintConfig,
6880
+ failOnError: lintConfig.failOnError ?? true
6881
+ });
6882
+ const result = await runner.run(tokens);
6883
+ if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6884
+ throw new exports.LintError(result.issues);
6516
6885
  }
6517
- };
6886
+ return result;
6887
+ }
6888
+ async function resolveAllPermutations(resolver) {
6889
+ const pipeline = createPipeline();
6890
+ const permutations = await pipeline.resolveAllPermutations(resolver);
6891
+ return permutations.map(({ tokens, modifierInputs }) => ({
6892
+ tokens: stripInternalTokenMetadata(tokens),
6893
+ modifierInputs
6894
+ }));
6895
+ }
6896
+ async function generateTypes(tokens, fileName, options) {
6897
+ const typeWriter = new TypeWriter();
6898
+ await typeWriter.write(tokens, {
6899
+ fileName,
6900
+ moduleName: options?.moduleName ?? "Tokens",
6901
+ includeValues: true
6902
+ });
6903
+ }
6518
6904
 
6519
6905
  // src/tokens/types.ts
6520
6906
  function isColorToken(token) {
@@ -6541,6 +6927,17 @@ function isTransitionToken(token) {
6541
6927
  function isGradientToken(token) {
6542
6928
  return token.$type === "gradient";
6543
6929
  }
6930
+ function nameKebabCase() {
6931
+ return {
6932
+ transform: (token) => {
6933
+ const name = changeCase.kebabCase(token.path.join(" "));
6934
+ return {
6935
+ ...token,
6936
+ name
6937
+ };
6938
+ }
6939
+ };
6940
+ }
6544
6941
  function isColorObject(value) {
6545
6942
  return typeof value === "object" && value !== null && "colorSpace" in value && "components" in value;
6546
6943
  }
@@ -6618,6 +7015,7 @@ function durationObjectToString(duration) {
6618
7015
  init_errors();
6619
7016
  init_token_utils();
6620
7017
  init_utils();
7018
+ init_metadata();
6621
7019
  var toSRGB = culori.converter("rgb");
6622
7020
  var toP3 = culori.converter("p3");
6623
7021
  var KOTLIN_KEYWORDS = /* @__PURE__ */ new Set([
@@ -6668,9 +7066,6 @@ function resolveColorFormat(format) {
6668
7066
  function escapeKotlinString(str) {
6669
7067
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\$/g, "\\$");
6670
7068
  }
6671
- function escapeKDoc(str) {
6672
- return str.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
6673
- }
6674
7069
  function formatKotlinNumber(value) {
6675
7070
  return Number.isInteger(value) ? `${value}.0` : String(value);
6676
7071
  }
@@ -6802,8 +7197,13 @@ var AndroidRenderer = class {
6802
7197
  const kotlinName = this.buildFlatKotlinName(token);
6803
7198
  const kotlinValue = this.formatKotlinValue(token, options, baseDepth + 1);
6804
7199
  const annotation = this.typeAnnotationSuffix(token);
6805
- if (token.$description) {
6806
- lines.push(`${valIndent}/** ${escapeKDoc(token.$description)} */`);
7200
+ const descriptionComment = buildTokenDescriptionComment(token, "kotlin");
7201
+ if (descriptionComment) {
7202
+ lines.push(`${valIndent}${descriptionComment}`);
7203
+ }
7204
+ const deprecation = buildKotlinDeprecationAnnotation(token);
7205
+ if (deprecation) {
7206
+ lines.push(`${valIndent}${deprecation}`);
6807
7207
  }
6808
7208
  lines.push(
6809
7209
  `${valIndent}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`
@@ -6839,8 +7239,13 @@ var AndroidRenderer = class {
6839
7239
  const kotlinName = toSafeIdentifier(key, KOTLIN_KEYWORDS, false);
6840
7240
  const kotlinValue = this.formatKotlinValue(token, options, depth);
6841
7241
  const annotation = this.typeAnnotationSuffix(token);
6842
- if (token.$description) {
6843
- lines.push(`${pad}/** ${escapeKDoc(token.$description)} */`);
7242
+ const descriptionComment = buildTokenDescriptionComment(token, "kotlin");
7243
+ if (descriptionComment) {
7244
+ lines.push(`${pad}${descriptionComment}`);
7245
+ }
7246
+ const deprecation = buildKotlinDeprecationAnnotation(token);
7247
+ if (deprecation) {
7248
+ lines.push(`${pad}${deprecation}`);
6844
7249
  }
6845
7250
  lines.push(`${pad}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`);
6846
7251
  }
@@ -7617,10 +8022,8 @@ function stableInputsKey(modifierInputs) {
7617
8022
 
7618
8023
  // src/renderers/css.ts
7619
8024
  init_utils();
8025
+ init_metadata();
7620
8026
  var CssRenderer = class _CssRenderer {
7621
- sanitizeCssCommentText(text) {
7622
- return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
7623
- }
7624
8027
  async format(context, options) {
7625
8028
  const opts = {
7626
8029
  preset: options?.preset ?? "bundle",
@@ -7700,12 +8103,13 @@ var CssRenderer = class _CssRenderer {
7700
8103
  }
7701
8104
  pushTokenLines(lines, token, tokens, referenceTokens, preserveReferences, indent, newline, space) {
7702
8105
  const entries = this.buildCssEntries(token, tokens, referenceTokens, preserveReferences);
7703
- if (token.$deprecated != null && token.$deprecated !== false) {
7704
- const deprecationMsg = formatDeprecationMessage(token, "", "comment");
7705
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(deprecationMsg)} */${newline}`);
8106
+ const deprecationComment = buildTokenDeprecationComment(token, "css");
8107
+ if (deprecationComment) {
8108
+ lines.push(`${indent}${deprecationComment}${newline}`);
7706
8109
  }
7707
- if (token.$description && token.$description !== "") {
7708
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(token.$description)} */${newline}`);
8110
+ const descriptionComment = buildTokenDescriptionComment(token, "css");
8111
+ if (descriptionComment) {
8112
+ lines.push(`${indent}${descriptionComment}${newline}`);
7709
8113
  }
7710
8114
  for (const entry of entries) {
7711
8115
  lines.push(`${indent}--${entry.name}:${space}${entry.value};${newline}`);
@@ -8345,6 +8749,7 @@ function cssRenderer() {
8345
8749
 
8346
8750
  // src/renderers/ios.ts
8347
8751
  init_utils();
8752
+ init_metadata();
8348
8753
  var toSRGB2 = culori.converter("rgb");
8349
8754
  var toP32 = culori.converter("p3");
8350
8755
  var SWIFT_TYPE_GROUP_MAP = {
@@ -8488,9 +8893,13 @@ var IosRenderer = class {
8488
8893
  const swiftValue = this.formatSwiftValue(token, options);
8489
8894
  const typeAnnotation = this.getTypeAnnotation(token);
8490
8895
  const annotation = typeAnnotation ? `: ${typeAnnotation}` : "";
8491
- const docComment = this.buildDocComment(token, indent);
8896
+ const docComment = buildTokenDescriptionComment(token, "swift");
8492
8897
  if (docComment) {
8493
- lines.push(docComment);
8898
+ lines.push(`${indent}${docComment}`);
8899
+ }
8900
+ const deprecationAttr = buildSwiftDeprecationAttribute(token);
8901
+ if (deprecationAttr) {
8902
+ lines.push(`${indent}${deprecationAttr}`);
8494
8903
  }
8495
8904
  lines.push(`${indent}${access3} ${staticPrefix}${swiftName}${annotation} = ${swiftValue}`);
8496
8905
  }
@@ -8505,15 +8914,6 @@ var IosRenderer = class {
8505
8914
  }
8506
8915
  return Array.from(imports).sort();
8507
8916
  }
8508
- /**
8509
- * Builds a `///` doc comment from a token's `$description`, if present.
8510
- */
8511
- buildDocComment(token, indent) {
8512
- if (!token.$description) {
8513
- return void 0;
8514
- }
8515
- return `${indent}/// ${token.$description}`;
8516
- }
8517
8917
  /**
8518
8918
  * Builds a qualified Swift name from a token's path, preserving parent
8519
8919
  * hierarchy segments to avoid duplicate identifiers.
@@ -8904,6 +9304,7 @@ function iosRenderer() {
8904
9304
  // src/renderers/js-module.ts
8905
9305
  init_utils();
8906
9306
  init_token_utils();
9307
+ init_metadata();
8907
9308
  var JsModuleRenderer = class {
8908
9309
  async format(context, options) {
8909
9310
  const opts = {
@@ -8941,17 +9342,10 @@ var JsModuleRenderer = class {
8941
9342
  return outputTree(files);
8942
9343
  }
8943
9344
  async formatTokens(tokens, options) {
8944
- const opts = {
8945
- preset: options.preset ?? "standalone",
8946
- structure: options.structure ?? "nested",
8947
- minify: options.minify ?? false,
8948
- moduleName: options.moduleName ?? "tokens",
8949
- generateHelper: options.generateHelper ?? false
8950
- };
8951
9345
  const lines = [];
8952
- lines.push(...this.formatAsObject(tokens, opts));
9346
+ lines.push(...this.formatAsObject(tokens, options));
8953
9347
  const code = lines.join("\n");
8954
- if (opts.minify) {
9348
+ if (options.minify) {
8955
9349
  return code;
8956
9350
  }
8957
9351
  return await prettier__default.default.format(code, {
@@ -8964,20 +9358,53 @@ var JsModuleRenderer = class {
8964
9358
  trailingComma: "es5"
8965
9359
  });
8966
9360
  }
8967
- /**
8968
- * Format as default export object
8969
- */
8970
9361
  formatAsObject(tokens, options) {
8971
9362
  const lines = [];
8972
- const tokenObj = this.tokensToPlainObject(tokens, options.structure);
9363
+ const tokenMap = this.buildTokenMap(tokens);
8973
9364
  const varName = options.moduleName !== "" ? options.moduleName : "tokens";
8974
- lines.push(`const ${varName} = {`);
8975
- this.addObjectProperties(lines, tokenObj, 1);
8976
- lines.push("}");
9365
+ if (options.structure === "flat") {
9366
+ lines.push(`const ${varName} = {`);
9367
+ this.addFlatProperties(lines, tokens, 1);
9368
+ lines.push("}");
9369
+ } else {
9370
+ lines.push(`const ${varName} = {`);
9371
+ const tokenObj = this.tokensToPlainObject(tokens, "nested");
9372
+ this.addNestedProperties(lines, tokenObj, tokenMap, 1);
9373
+ lines.push("}");
9374
+ }
8977
9375
  lines.push("");
8978
9376
  lines.push(`export default ${varName}`);
8979
9377
  return lines;
8980
9378
  }
9379
+ buildTokenMap(tokens) {
9380
+ const map = /* @__PURE__ */ new Map();
9381
+ for (const [name, token] of Object.entries(tokens)) {
9382
+ map.set(name, token);
9383
+ }
9384
+ return map;
9385
+ }
9386
+ addFlatProperties(lines, tokens, indent) {
9387
+ const indentStr2 = " ".repeat(indent);
9388
+ const sortedEntries = getSortedTokenEntries(tokens);
9389
+ for (let i = 0; i < sortedEntries.length; i++) {
9390
+ const [name, token] = sortedEntries[i];
9391
+ const isLast = i === sortedEntries.length - 1;
9392
+ this.pushTokenComments(lines, token, indentStr2);
9393
+ lines.push(
9394
+ `${indentStr2}${this.quoteKey(name)}: ${JSON.stringify(token.$value)}${isLast ? "" : ","}`
9395
+ );
9396
+ }
9397
+ }
9398
+ pushTokenComments(lines, token, indent) {
9399
+ const deprecationComment = buildTokenDeprecationComment(token, "js");
9400
+ if (deprecationComment) {
9401
+ lines.push(`${indent}${deprecationComment}`);
9402
+ }
9403
+ const descriptionComment = buildTokenDescriptionComment(token, "js");
9404
+ if (descriptionComment) {
9405
+ lines.push(`${indent}${descriptionComment}`);
9406
+ }
9407
+ }
8981
9408
  tokensToPlainObject(tokens, structure) {
8982
9409
  if (structure === "nested") {
8983
9410
  return buildNestedTokenObject(tokens, (token) => token.$value);
@@ -8988,7 +9415,7 @@ var JsModuleRenderer = class {
8988
9415
  }
8989
9416
  return result;
8990
9417
  }
8991
- addObjectProperties(lines, obj, indent) {
9418
+ addNestedProperties(lines, obj, tokenMap, indent) {
8992
9419
  const indentStr2 = " ".repeat(indent);
8993
9420
  const entries = Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
8994
9421
  for (let i = 0; i < entries.length; i++) {
@@ -9000,19 +9427,20 @@ var JsModuleRenderer = class {
9000
9427
  const isLast = i === entries.length - 1;
9001
9428
  const isNestedObject = typeof value === "object" && value !== null && !Array.isArray(value);
9002
9429
  if (!isNestedObject) {
9430
+ const token = tokenMap.get(key);
9431
+ if (token) {
9432
+ this.pushTokenComments(lines, token, indentStr2);
9433
+ }
9003
9434
  lines.push(
9004
9435
  `${indentStr2}${this.quoteKey(key)}: ${JSON.stringify(value)}${isLast ? "" : ","}`
9005
9436
  );
9006
9437
  continue;
9007
9438
  }
9008
9439
  lines.push(`${indentStr2}${this.quoteKey(key)}: {`);
9009
- this.addObjectProperties(lines, value, indent + 1);
9440
+ this.addNestedProperties(lines, value, tokenMap, indent + 1);
9010
9441
  lines.push(`${indentStr2}}${isLast ? "" : ","}`);
9011
9442
  }
9012
9443
  }
9013
- /**
9014
- * Quote key if necessary
9015
- */
9016
9444
  quoteKey(key) {
9017
9445
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
9018
9446
  return key;
@@ -9248,6 +9676,7 @@ function resolveOptions(options) {
9248
9676
 
9249
9677
  // src/renderers/tailwind.ts
9250
9678
  init_utils();
9679
+ init_metadata();
9251
9680
  var TAILWIND_NAMESPACE_MAP = {
9252
9681
  color: "color",
9253
9682
  dimension: "spacing",
@@ -9301,6 +9730,14 @@ var TailwindRenderer = class {
9301
9730
  for (const [, token] of getSortedTokenEntries(tokens)) {
9302
9731
  const varName = this.buildVariableName(token);
9303
9732
  const varValue = this.formatValue(token);
9733
+ const deprecationComment = buildTokenDeprecationComment(token, "tailwind");
9734
+ if (deprecationComment) {
9735
+ lines.push(`${indent}${deprecationComment}${newline}`);
9736
+ }
9737
+ const descriptionComment = buildTokenDescriptionComment(token, "tailwind");
9738
+ if (descriptionComment) {
9739
+ lines.push(`${indent}${descriptionComment}${newline}`);
9740
+ }
9304
9741
  lines.push(`${indent}--${varName}:${space}${varValue};${newline}`);
9305
9742
  }
9306
9743
  lines.push(`}${newline}`);
@@ -9327,6 +9764,14 @@ var TailwindRenderer = class {
9327
9764
  for (const [, token] of getSortedTokenEntries(tokens)) {
9328
9765
  const varName = this.buildVariableName(token);
9329
9766
  const varValue = this.formatValue(token);
9767
+ const deprecationComment = buildTokenDeprecationComment(token, "tailwind");
9768
+ if (deprecationComment) {
9769
+ lines.push(`${tokenIndent}${deprecationComment}${newline}`);
9770
+ }
9771
+ const descriptionComment = buildTokenDescriptionComment(token, "tailwind");
9772
+ if (descriptionComment) {
9773
+ lines.push(`${tokenIndent}${descriptionComment}${newline}`);
9774
+ }
9330
9775
  lines.push(`${tokenIndent}--${varName}:${space}${varValue};${newline}`);
9331
9776
  }
9332
9777
  if (hasMediaQuery) {
@@ -9487,7 +9932,7 @@ function css(config) {
9487
9932
  file,
9488
9933
  renderer: cssRenderer(),
9489
9934
  options: { preset, ...rendererOptions },
9490
- transforms,
9935
+ transforms: [nameKebabCase(), ...transforms ?? []],
9491
9936
  filters,
9492
9937
  hooks
9493
9938
  };
@@ -9599,16 +10044,26 @@ init_errors();
9599
10044
  * This source code is licensed under the MIT license found in the
9600
10045
  * LICENSE file in the root directory of this source tree.
9601
10046
  */
10047
+ /**
10048
+ * @license MIT
10049
+ * Copyright (c) 2025-present Dispersa
10050
+ *
10051
+ * This source code is licensed under the MIT license found in the
10052
+ * LICENSE file in the root directory of this source tree.
10053
+ */
9602
10054
  /**
9603
10055
  * @license
9604
10056
  * Copyright (c) 2025 Dispersa Contributors
9605
10057
  * SPDX-License-Identifier: MIT
9606
10058
  */
9607
10059
 
9608
- exports.Dispersa = Dispersa;
9609
10060
  exports.android = android;
10061
+ exports.build = build;
10062
+ exports.buildOrThrow = buildOrThrow;
10063
+ exports.buildPermutation = buildPermutation;
9610
10064
  exports.css = css;
9611
10065
  exports.defineRenderer = defineRenderer;
10066
+ exports.generateTypes = generateTypes;
9612
10067
  exports.ios = ios;
9613
10068
  exports.isBorderToken = isBorderToken;
9614
10069
  exports.isColorToken = isColorToken;
@@ -9621,7 +10076,10 @@ exports.isTransitionToken = isTransitionToken;
9621
10076
  exports.isTypographyToken = isTypographyToken;
9622
10077
  exports.js = js;
9623
10078
  exports.json = json;
10079
+ exports.lint = lint;
9624
10080
  exports.outputTree = outputTree;
10081
+ exports.resolveAllPermutations = resolveAllPermutations;
10082
+ exports.resolveTokens = resolveTokens;
9625
10083
  exports.tailwind = tailwind;
9626
10084
  //# sourceMappingURL=index.cjs.map
9627
10085
  //# sourceMappingURL=index.cjs.map