dispersa 0.4.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +65 -30
  2. package/dist/android-CRDfSB3_.d.cts +126 -0
  3. package/dist/android-DANJjjPO.d.ts +126 -0
  4. package/dist/builders.cjs +206 -62
  5. package/dist/builders.cjs.map +1 -1
  6. package/dist/builders.d.cts +12 -11
  7. package/dist/builders.d.ts +12 -11
  8. package/dist/builders.js +206 -62
  9. package/dist/builders.js.map +1 -1
  10. package/dist/cli/cli.js +120 -7
  11. package/dist/cli/cli.js.map +1 -1
  12. package/dist/cli/config.d.ts +321 -0
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/index.js +119 -7
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/dispersa-BC1kDF5u.d.ts +118 -0
  17. package/dist/dispersa-DL3J_Pmz.d.cts +118 -0
  18. package/dist/errors-qT4sJgSA.d.cts +104 -0
  19. package/dist/errors-qT4sJgSA.d.ts +104 -0
  20. package/dist/errors.cjs.map +1 -1
  21. package/dist/errors.d.cts +1 -83
  22. package/dist/errors.d.ts +1 -83
  23. package/dist/errors.js.map +1 -1
  24. package/dist/filters.cjs.map +1 -1
  25. package/dist/filters.d.cts +2 -2
  26. package/dist/filters.d.ts +2 -2
  27. package/dist/filters.js.map +1 -1
  28. package/dist/{index-CNT2Meyf.d.cts → index-Dajm5rvM.d.ts} +311 -132
  29. package/dist/{index-CqdaN3X0.d.ts → index-De6SjZYH.d.cts} +311 -132
  30. package/dist/index.cjs +799 -353
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +8 -329
  33. package/dist/index.d.ts +8 -329
  34. package/dist/index.js +793 -353
  35. package/dist/index.js.map +1 -1
  36. package/dist/lint.cjs +1017 -0
  37. package/dist/lint.cjs.map +1 -0
  38. package/dist/lint.d.cts +463 -0
  39. package/dist/lint.d.ts +463 -0
  40. package/dist/lint.js +997 -0
  41. package/dist/lint.js.map +1 -0
  42. package/dist/preprocessors.d.cts +2 -2
  43. package/dist/preprocessors.d.ts +2 -2
  44. package/dist/renderers.cjs.map +1 -1
  45. package/dist/renderers.d.cts +7 -6
  46. package/dist/renderers.d.ts +7 -6
  47. package/dist/renderers.js.map +1 -1
  48. package/dist/transforms.d.cts +2 -2
  49. package/dist/transforms.d.ts +2 -2
  50. package/dist/{types-CZb19kiq.d.ts → types-8MLtztK3.d.ts} +56 -1
  51. package/dist/{types-CussyWwe.d.cts → types-BHBHRm0a.d.cts} +56 -1
  52. package/dist/{types-BAv39mum.d.cts → types-BltzwVYK.d.cts} +1 -1
  53. package/dist/{types-DWKq-eJj.d.cts → types-CAdUV-fa.d.cts} +1 -1
  54. package/dist/{types-CzHa7YkW.d.ts → types-DztXKlka.d.ts} +1 -1
  55. package/dist/{types-Bc0kA7De.d.ts → types-TQHV1MrY.d.cts} +19 -1
  56. package/dist/{types-Bc0kA7De.d.cts → types-TQHV1MrY.d.ts} +19 -1
  57. package/dist/{types-BzNcG-rI.d.ts → types-ebxDimRz.d.ts} +1 -1
  58. package/package.json +11 -1
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ 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');
9
12
  var changeCase = require('change-case');
10
13
  var culori = require('culori');
@@ -33,6 +36,7 @@ function _interopNamespace(e) {
33
36
  var Ajv__default = /*#__PURE__*/_interopDefault(Ajv);
34
37
  var addFormats__default = /*#__PURE__*/_interopDefault(addFormats);
35
38
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
39
+ var process2__default = /*#__PURE__*/_interopDefault(process2);
36
40
  var prettier__default = /*#__PURE__*/_interopDefault(prettier);
37
41
 
38
42
  var __defProp = Object.defineProperty;
@@ -46,7 +50,7 @@ var __export = (target, all) => {
46
50
  };
47
51
 
48
52
  // src/shared/errors/index.ts
49
- 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;
50
54
  var init_errors = __esm({
51
55
  "src/shared/errors/index.ts"() {
52
56
  exports.DispersaError = class extends Error {
@@ -129,23 +133,19 @@ var init_errors = __esm({
129
133
  this.name = "ModifierError";
130
134
  }
131
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
+ };
132
145
  }
133
146
  });
134
147
 
135
148
  // src/shared/utils/token-utils.ts
136
- function formatDeprecationMessage(token, description = "", format = "bracket") {
137
- if (token.$deprecated == null || token.$deprecated === false) {
138
- return description;
139
- }
140
- const deprecationMsg = typeof token.$deprecated === "string" ? token.$deprecated : "";
141
- if (format === "comment") {
142
- const msg2 = deprecationMsg ? ` ${deprecationMsg}` : "";
143
- return `DEPRECATED${msg2}`;
144
- }
145
- const msg = deprecationMsg ? `: ${deprecationMsg}` : "";
146
- const prefix = `[DEPRECATED${msg}]`;
147
- return description ? `${prefix} ${description}` : prefix;
148
- }
149
149
  function stripInternalTokenMetadata(tokens) {
150
150
  const cleaned = {};
151
151
  for (const [name, token] of Object.entries(tokens)) {
@@ -2499,7 +2499,7 @@ var init_schemas = __esm({
2499
2499
  });
2500
2500
 
2501
2501
  // src/validation/config-schemas.ts
2502
- var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2502
+ var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, lintConfigSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2503
2503
  var init_config_schemas = __esm({
2504
2504
  "src/validation/config-schemas.ts"() {
2505
2505
  init_schemas();
@@ -2650,6 +2650,42 @@ var init_config_schemas = __esm({
2650
2650
  }
2651
2651
  }
2652
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
+ };
2653
2689
  outputConfigSchema = {
2654
2690
  $schema: "http://json-schema.org/draft-07/schema#",
2655
2691
  type: "object",
@@ -2767,10 +2803,20 @@ var init_config_schemas = __esm({
2767
2803
  description: "Resolver configuration - file path or ResolverDocument object"
2768
2804
  },
2769
2805
  buildPath: { type: "string" },
2806
+ validation: {
2807
+ type: "object",
2808
+ properties: {
2809
+ mode: { type: "string", enum: ["error", "warn", "off"] }
2810
+ }
2811
+ },
2770
2812
  hooks: {
2771
2813
  type: "object",
2772
2814
  description: "Global build lifecycle hooks (functions, validated at runtime)",
2773
2815
  additionalProperties: true
2816
+ },
2817
+ lint: {
2818
+ ...lintConfigSchema,
2819
+ description: "Linting configuration"
2774
2820
  }
2775
2821
  },
2776
2822
  additionalProperties: false
@@ -3530,6 +3576,94 @@ var init_utils = __esm({
3530
3576
  }
3531
3577
  });
3532
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
+
3533
3667
  // src/renderers/bundlers/js.ts
3534
3668
  var js_exports = {};
3535
3669
  __export(js_exports, {
@@ -3634,7 +3768,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3634
3768
  }
3635
3769
  const metadata = buildMetadata(resolver);
3636
3770
  const jsBlocks = [];
3637
- for (const { tokens, modifierInputs } of bundleData) {
3771
+ for (const { tokens, modifierInputs, isBase } of bundleData) {
3638
3772
  const cleanTokens = stripInternalMetadata(tokens);
3639
3773
  const key = buildStableDashKey({
3640
3774
  modifierInputs,
@@ -3642,10 +3776,30 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3642
3776
  defaults: metadata.defaults
3643
3777
  });
3644
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
+ }
3645
3787
  const formattedJs = await formatTokens(cleanTokens);
3646
3788
  const tokenObject = extractObjectFromJsModule(formattedJs);
3647
3789
  const indentedObject = tokenObject.replace(/\n/g, "\n ");
3648
- 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}
3649
3803
  ${JSON.stringify(camelKey)}: ${indentedObject}`);
3650
3804
  }
3651
3805
  return assembleJsBundle(metadata, jsBlocks, options?.generateHelper ?? false);
@@ -3653,6 +3807,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3653
3807
  var init_js = __esm({
3654
3808
  "src/renderers/bundlers/js.ts"() {
3655
3809
  init_errors();
3810
+ init_metadata();
3656
3811
  init_utils();
3657
3812
  }
3658
3813
  });
@@ -4022,7 +4177,8 @@ var BuildOrchestrator = class {
4022
4177
  resolver,
4023
4178
  config.transforms,
4024
4179
  config.preprocessors,
4025
- config.filters
4180
+ config.filters,
4181
+ config.lint
4026
4182
  );
4027
4183
  return this.executeBuild(buildPath, config, permutations2, resolver);
4028
4184
  }
@@ -4033,7 +4189,8 @@ var BuildOrchestrator = class {
4033
4189
  modifierInputs,
4034
4190
  config.transforms,
4035
4191
  config.preprocessors,
4036
- config.filters
4192
+ config.filters,
4193
+ config.lint
4037
4194
  );
4038
4195
  return { tokens, modifierInputs: resolvedInputs };
4039
4196
  })
@@ -4294,7 +4451,358 @@ var OutputProcessor = class {
4294
4451
 
4295
4452
  // src/build/pipeline/token-pipeline.ts
4296
4453
  init_resolver_loader();
4297
- 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
+ };
4298
4806
 
4299
4807
  // src/shared/constants.ts
4300
4808
  var DEFAULT_MAX_ALIAS_DEPTH = 10;
@@ -5489,6 +5997,10 @@ var ResolutionEngine = class {
5489
5997
  return typeof obj === "object" && obj !== null && "contexts" in obj && typeof obj.contexts === "object";
5490
5998
  }
5491
5999
  };
6000
+
6001
+ // src/build/pipeline/token-pipeline.ts
6002
+ init_errors();
6003
+ init_validation_handler();
5492
6004
  init_errors();
5493
6005
 
5494
6006
  // src/shared/utils/path-utils.ts
@@ -5981,6 +6493,8 @@ var TokenPipeline = class {
5981
6493
  resolverLoader;
5982
6494
  tokenParser;
5983
6495
  aliasResolver;
6496
+ lintRunner = null;
6497
+ lintConfigCache = null;
5984
6498
  constructor(options = {}) {
5985
6499
  this.options = options;
5986
6500
  this.validationHandler = new ValidationHandler(options.validation);
@@ -6000,8 +6514,9 @@ var TokenPipeline = class {
6000
6514
  * 6. Parse and flatten token structure
6001
6515
  * 7. Resolve alias references
6002
6516
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
6003
- * 9. Apply filters (if provided) — runs first to remove tokens before transforms
6004
- * 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
6005
6520
  *
6006
6521
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
6007
6522
  *
@@ -6010,9 +6525,11 @@ var TokenPipeline = class {
6010
6525
  * @param transformList - Optional transforms to apply
6011
6526
  * @param preprocessorList - Optional preprocessors to apply
6012
6527
  * @param filterList - Optional filters to apply before transforms
6013
- * @returns Final tokens and resolution engine
6528
+ * @param lintConfig - Optional lint configuration for this run
6529
+ * @returns Final tokens, resolution engine, and lint result
6014
6530
  */
6015
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6531
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6532
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6016
6533
  const stage1 = await this.loadResolver(resolver);
6017
6534
  const engine = this.createEngine(stage1);
6018
6535
  const result = await this.runPipelineStages(
@@ -6020,29 +6537,32 @@ var TokenPipeline = class {
6020
6537
  modifierInputs,
6021
6538
  preprocessorList,
6022
6539
  filterList,
6023
- transformList
6540
+ transformList,
6541
+ effectiveLintConfig
6024
6542
  );
6025
6543
  return {
6026
6544
  tokens: result.tokens,
6027
6545
  resolutionEngine: result.resolutionEngine,
6028
- modifierInputs: result.modifierInputs
6546
+ modifierInputs: result.modifierInputs,
6547
+ lintResult: result.lintResult
6029
6548
  };
6030
6549
  }
6031
6550
  /**
6032
- * Run pipeline stages 3-9 on a pre-created engine
6551
+ * Run pipeline stages 3-11 on a pre-created engine
6033
6552
  *
6034
6553
  * Shared by both `resolve()` (single permutation) and
6035
6554
  * `resolveAllPermutations()` (parallel permutations) to keep the
6036
6555
  * stage sequence defined in exactly one place.
6037
6556
  */
6038
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6557
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6039
6558
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6040
6559
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6041
6560
  const refResolved = await this.resolveReferences(preprocessed);
6042
6561
  const flattened = this.flattenTokens(refResolved);
6043
6562
  const aliasResolved = this.resolveAliases(flattened);
6044
6563
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6045
- const filtered = this.applyFilterStage(rootStripped, filterList);
6564
+ const linted = await this.runLintStage(rootStripped, lintConfig);
6565
+ const filtered = this.applyFilterStage(linted, filterList);
6046
6566
  return this.applyTransformStage(filtered, transformList);
6047
6567
  }
6048
6568
  /**
@@ -6155,6 +6675,47 @@ var TokenPipeline = class {
6155
6675
  }
6156
6676
  return { ...stage, aliasResolvedTokens: result };
6157
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
+ */
6158
6719
  applyFilterStage(stage, filterList) {
6159
6720
  let tokens = stage.aliasResolvedTokens;
6160
6721
  if (filterList !== void 0 && filterList.length > 0) {
@@ -6163,7 +6724,7 @@ var TokenPipeline = class {
6163
6724
  return { ...stage, aliasResolvedTokens: tokens };
6164
6725
  }
6165
6726
  /**
6166
- * Stage 9: Apply transforms to the filtered token set
6727
+ * Stage 11: Apply transforms to the filtered token set
6167
6728
  */
6168
6729
  applyTransformStage(stage, transformList) {
6169
6730
  let tokens = stage.aliasResolvedTokens;
@@ -6197,8 +6758,10 @@ var TokenPipeline = class {
6197
6758
  * @param transformList - Optional transforms to apply
6198
6759
  * @param preprocessorList - Optional preprocessors to apply
6199
6760
  * @param filterList - Optional filters to apply before transforms
6761
+ * @param lintConfig - Optional lint configuration for this run
6200
6762
  */
6201
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6763
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6764
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6202
6765
  const stage1 = await this.loadResolver(resolver);
6203
6766
  const discoveryEngine = this.createEngine(stage1);
6204
6767
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6211,11 +6774,13 @@ var TokenPipeline = class {
6211
6774
  modifierInputs,
6212
6775
  preprocessorList,
6213
6776
  filterList,
6214
- transformList
6777
+ transformList,
6778
+ effectiveLintConfig
6215
6779
  );
6216
6780
  return {
6217
6781
  tokens: result.tokens,
6218
- modifierInputs: result.modifierInputs
6782
+ modifierInputs: result.modifierInputs,
6783
+ lintResult: result.lintResult
6219
6784
  };
6220
6785
  })
6221
6786
  );
@@ -6236,286 +6801,106 @@ var TokenPipeline = class {
6236
6801
  init_errors();
6237
6802
  init_token_utils();
6238
6803
  init_validator();
6239
- var Dispersa = class {
6240
- validator;
6241
- pipeline;
6242
- outputProcessor;
6243
- orchestrator;
6244
- options;
6245
- /**
6246
- * Creates a new Dispersa instance
6247
- *
6248
- * @param options - Configuration options (optional, can be empty object)
6249
- * @param options.resolver - Default resolver (file path or inline object, optional if provided at build time)
6250
- * @param options.buildPath - Default output directory for generated files (omit for in-memory mode)
6251
- * @throws {ConfigurationError} If options are invalid and validation is enabled
6252
- */
6253
- constructor(options = {}) {
6254
- this.options = options;
6255
- this.validator = new SchemaValidator();
6256
- const errors = this.validator.validateDispersaOptions(options);
6257
- if (errors.length > 0) {
6258
- throw new exports.ConfigurationError(
6259
- `Invalid Dispersa options: ${this.validator.getErrorMessage(errors)}`
6260
- );
6261
- }
6262
- this.pipeline = new TokenPipeline({ validation: options.validation });
6263
- this.outputProcessor = new OutputProcessor();
6264
- this.orchestrator = new BuildOrchestrator(this.pipeline, this.outputProcessor);
6265
- }
6266
- /**
6267
- * Builds design tokens from a resolver configuration
6268
- *
6269
- * Processes tokens through the resolution pipeline, applies preprocessors,
6270
- * transforms, and filters, then generates output files in multiple formats
6271
- * for specified outputs.
6272
- *
6273
- * **Runtime Validation:**
6274
- * This method validates the build configuration
6275
- * and all output configurations before processing, throwing helpful errors for
6276
- * invalid inputs.
6277
- *
6278
- * **Permutation Handling:**
6279
- * - If `config.permutations` is provided and non-empty, builds those specific permutations
6280
- * - If `config.permutations` is undefined or empty, auto-discovers all permutations from resolver
6281
- *
6282
- * @param config - Build configuration
6283
- * @param config.resolver - Resolver configuration (file path or inline object, optional if set in constructor)
6284
- * @param config.outputs - Array of output configurations (renderers, transforms, filters)
6285
- * @param config.buildPath - Output directory for generated files (omit for in-memory mode, optional if set in constructor)
6286
- * @param config.transforms - Global transforms to apply to all tokens
6287
- * @param config.preprocessors - Global preprocessors to apply before parsing
6288
- * @param config.permutations - Array of modifier inputs for generating variations
6289
- * @returns Build result with success status and generated output files
6290
- * @throws {ConfigurationError} If configuration is invalid
6291
- * @throws {FileOperationError} If file operations fail
6292
- *
6293
- * @example Basic build
6294
- * ```typescript
6295
- * const dispersa = new Dispersa({
6296
- * resolver: './tokens.resolver.json',
6297
- * buildPath: './output'
6298
- * })
6299
- *
6300
- * const result = await dispersa.build({
6301
- * outputs: [
6302
- * css({
6303
- * name: 'css',
6304
- * file: 'tokens.css',
6305
- * preset: 'bundle',
6306
- * selector: ':root',
6307
- * transforms: [nameKebabCase()]
6308
- * })
6309
- * ]
6310
- * })
6311
- * ```
6312
- *
6313
- * @example With filters and multiple presets
6314
- * ```typescript
6315
- * const result = await dispersa.build({
6316
- * resolver: './tokens.resolver.json',
6317
- * outputs: [
6318
- * css({
6319
- * name: 'css',
6320
- * file: 'tokens.css',
6321
- * preset: 'bundle',
6322
- * selector: ':root', // All themes in one file
6323
- * transforms: [nameKebabCase()]
6324
- * }),
6325
- * json({
6326
- * name: 'json',
6327
- * file: 'tokens-{theme}.json', // Separate file per theme
6328
- * preset: 'standalone',
6329
- * structure: 'flat'
6330
- * })
6331
- * ],
6332
- * buildPath: './output',
6333
- * permutations: [
6334
- * { theme: 'light' },
6335
- * { theme: 'dark' }
6336
- * ]
6337
- * })
6338
- * ```
6339
- */
6340
- async build(config) {
6341
- try {
6342
- return await this.buildOrThrow(config);
6343
- } catch (error) {
6344
- return {
6345
- success: false,
6346
- outputs: [],
6347
- errors: [toBuildError(error)]
6348
- };
6349
- }
6350
- }
6351
- /**
6352
- * Builds design tokens and throws on any failure.
6353
- *
6354
- * Unlike {@link build}, which catches errors and returns them inside
6355
- * `BuildResult.errors`, this method propagates the first error as an
6356
- * exception. Use it when you want fail-fast behavior in CLI or CI workflows.
6357
- *
6358
- * @param config - Build configuration specifying resolver, outputs, transforms, etc.
6359
- * @returns A successful `BuildResult` (never contains errors)
6360
- * @throws {ConfigurationError} When the build config or resolver is invalid
6361
- * @throws {DispersaError} When token resolution, transforms, or rendering fails
6362
- *
6363
- * @example
6364
- * ```typescript
6365
- * try {
6366
- * const result = await dispersa.buildOrThrow({
6367
- * resolver: './tokens.resolver.json',
6368
- * outputs: [css({ name: 'css', file: 'tokens.css' })],
6369
- * buildPath: './output',
6370
- * })
6371
- * } catch (error) {
6372
- * process.exit(1)
6373
- * }
6374
- * ```
6375
- */
6376
- async buildOrThrow(config) {
6377
- const configErrors = this.validator.validateBuildConfig(config);
6378
- if (configErrors.length > 0) {
6379
- throw new exports.ConfigurationError(
6380
- `Invalid build configuration: ${this.validator.getErrorMessage(configErrors)}`
6381
- );
6382
- }
6383
- for (const output of config.outputs) {
6384
- const outputErrors = this.validator.validateOutputConfig(output);
6385
- if (outputErrors.length > 0) {
6386
- throw new exports.ConfigurationError(
6387
- `Invalid output '${output.name}': ${this.validator.getErrorMessage(outputErrors)}`
6388
- );
6389
- }
6390
- }
6391
- const { resolver, buildPath } = this.resolveConfig(config);
6392
- 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");
6393
6821
  }
6394
- /**
6395
- * Builds tokens for a single permutation with all configured outputs
6396
- *
6397
- * Convenience wrapper around `build()` that forces a single permutation.
6398
- * This means it has the same runtime validation and error semantics as `build()`:
6399
- * it returns a `BuildResult` object rather than throwing (use `buildOrThrow()` if you
6400
- * want fail-fast behavior).
6401
- *
6402
- * @param config - Build configuration
6403
- * @param modifierInputs - Modifier values (e.g., `{ theme: 'dark' }`)
6404
- * @returns Build result (success, outputs, optional errors)
6405
- */
6406
- async buildPermutation(config, modifierInputs = {}) {
6407
- return await this.build({
6408
- ...config,
6409
- permutations: [modifierInputs]
6410
- });
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
+ );
6411
6830
  }
6412
- /**
6413
- * Resolve configuration with constructor defaults
6414
- */
6415
- resolveConfig(config) {
6416
- const resolver = config.resolver ?? this.options.resolver;
6417
- const buildPath = config.buildPath ?? this.options.buildPath ?? "";
6418
- if (!resolver) {
6831
+ for (const output of config.outputs) {
6832
+ const outputErrors = validator.validateOutputConfig(output);
6833
+ if (outputErrors.length > 0) {
6419
6834
  throw new exports.ConfigurationError(
6420
- "resolver must be provided either in constructor options or build config"
6835
+ `Invalid output '${output.name}': ${validator.getErrorMessage(outputErrors)}`
6421
6836
  );
6422
6837
  }
6423
- return { resolver, buildPath };
6424
6838
  }
6425
- /**
6426
- * Resolves tokens for a specific permutation without generating output files
6427
- *
6428
- * Useful for programmatic access to resolved token values, testing,
6429
- * or implementing custom output logic. Returns fully resolved tokens
6430
- * with all references and aliases resolved.
6431
- *
6432
- * @param resolver - Resolver configuration (file path or inline object)
6433
- * @param modifierInputs - Modifier values for this permutation (e.g., `{ theme: 'dark' }`)
6434
- * @returns Object mapping token names to resolved token objects
6435
- * @throws {FileOperationError} If resolver file cannot be read
6436
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6437
- *
6438
- * @example
6439
- * ```typescript
6440
- * const tokens = await dispersa.resolveTokens(
6441
- * './tokens.resolver.json',
6442
- * { theme: 'dark' }
6443
- * )
6444
- *
6445
- * console.log(tokens['color.background'].$value) // '#1a1a1a'
6446
- * ```
6447
- */
6448
- async resolveTokens(resolver, modifierInputs = {}) {
6449
- const { tokens } = await this.pipeline.resolve(resolver, modifierInputs);
6450
- return stripInternalTokenMetadata(tokens);
6451
- }
6452
- /**
6453
- * Resolves tokens for all permutations defined in the resolver
6454
- *
6455
- * Auto-discovers all possible permutations from the resolver's modifier
6456
- * definitions and resolves tokens for each one. Useful for generating
6457
- * comprehensive token sets or validating all theme variations.
6458
- *
6459
- * @param resolver - Resolver configuration (file path or inline object)
6460
- * @returns Array of resolved token sets with their modifier inputs
6461
- * @throws {FileOperationError} If resolver file cannot be read
6462
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6463
- *
6464
- * @example
6465
- * ```typescript
6466
- * const permutations = await dispersa.resolveAllPermutations(
6467
- * './tokens.resolver.json'
6468
- * )
6469
- *
6470
- * permutations.forEach(({ tokens, modifierInputs }) => {
6471
- * console.log(`Theme: ${modifierInputs.theme}`)
6472
- * console.log(`Tokens: ${Object.keys(tokens).length}`)
6473
- * })
6474
- * ```
6475
- */
6476
- async resolveAllPermutations(resolver) {
6477
- const permutations = await this.pipeline.resolveAllPermutations(resolver);
6478
- return permutations.map(({ tokens, modifierInputs }) => ({
6479
- tokens: stripInternalTokenMetadata(tokens),
6480
- modifierInputs
6481
- }));
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
+ };
6482
6853
  }
6483
- /**
6484
- * Generates TypeScript type definitions from resolved tokens
6485
- *
6486
- * Creates a `.d.ts` file with type-safe token definitions including:
6487
- * - Token name union type for autocomplete
6488
- * - Token value types
6489
- * - Nested structure types matching token organization
6490
- *
6491
- * @param tokens - Resolved tokens object
6492
- * @param fileName - Path for the generated `.d.ts` file
6493
- * @param options - Generation options
6494
- * @param options.moduleName - Name for the exported types (default: 'Tokens')
6495
- * @returns Promise that resolves when file is written
6496
- * @throws {FileOperationError} If file cannot be written
6497
- *
6498
- * @example
6499
- * ```typescript
6500
- * const tokens = await dispersa.resolveTokens('./tokens.resolver.json')
6501
- * await dispersa.generateTypes(tokens, './src/tokens.d.ts', {
6502
- * moduleName: 'DesignTokens'
6503
- * })
6504
- *
6505
- * // Generated types can be used like:
6506
- * // const tokenName: TokenName = 'color.background'
6507
- * // const tokens: DesignTokens = { ... }
6508
- * ```
6509
- */
6510
- async generateTypes(tokens, fileName, options) {
6511
- const typeWriter = new TypeWriter();
6512
- await typeWriter.write(tokens, {
6513
- fileName,
6514
- moduleName: options?.moduleName ?? "Tokens",
6515
- includeValues: true
6516
- });
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);
6517
6885
  }
6518
- };
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
+ }
6519
6904
 
6520
6905
  // src/tokens/types.ts
6521
6906
  function isColorToken(token) {
@@ -6630,6 +7015,7 @@ function durationObjectToString(duration) {
6630
7015
  init_errors();
6631
7016
  init_token_utils();
6632
7017
  init_utils();
7018
+ init_metadata();
6633
7019
  var toSRGB = culori.converter("rgb");
6634
7020
  var toP3 = culori.converter("p3");
6635
7021
  var KOTLIN_KEYWORDS = /* @__PURE__ */ new Set([
@@ -6680,9 +7066,6 @@ function resolveColorFormat(format) {
6680
7066
  function escapeKotlinString(str) {
6681
7067
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\$/g, "\\$");
6682
7068
  }
6683
- function escapeKDoc(str) {
6684
- return str.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
6685
- }
6686
7069
  function formatKotlinNumber(value) {
6687
7070
  return Number.isInteger(value) ? `${value}.0` : String(value);
6688
7071
  }
@@ -6814,8 +7197,13 @@ var AndroidRenderer = class {
6814
7197
  const kotlinName = this.buildFlatKotlinName(token);
6815
7198
  const kotlinValue = this.formatKotlinValue(token, options, baseDepth + 1);
6816
7199
  const annotation = this.typeAnnotationSuffix(token);
6817
- if (token.$description) {
6818
- 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}`);
6819
7207
  }
6820
7208
  lines.push(
6821
7209
  `${valIndent}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`
@@ -6851,8 +7239,13 @@ var AndroidRenderer = class {
6851
7239
  const kotlinName = toSafeIdentifier(key, KOTLIN_KEYWORDS, false);
6852
7240
  const kotlinValue = this.formatKotlinValue(token, options, depth);
6853
7241
  const annotation = this.typeAnnotationSuffix(token);
6854
- if (token.$description) {
6855
- 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}`);
6856
7249
  }
6857
7250
  lines.push(`${pad}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`);
6858
7251
  }
@@ -7629,10 +8022,8 @@ function stableInputsKey(modifierInputs) {
7629
8022
 
7630
8023
  // src/renderers/css.ts
7631
8024
  init_utils();
8025
+ init_metadata();
7632
8026
  var CssRenderer = class _CssRenderer {
7633
- sanitizeCssCommentText(text) {
7634
- return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
7635
- }
7636
8027
  async format(context, options) {
7637
8028
  const opts = {
7638
8029
  preset: options?.preset ?? "bundle",
@@ -7712,12 +8103,13 @@ var CssRenderer = class _CssRenderer {
7712
8103
  }
7713
8104
  pushTokenLines(lines, token, tokens, referenceTokens, preserveReferences, indent, newline, space) {
7714
8105
  const entries = this.buildCssEntries(token, tokens, referenceTokens, preserveReferences);
7715
- if (token.$deprecated != null && token.$deprecated !== false) {
7716
- const deprecationMsg = formatDeprecationMessage(token, "", "comment");
7717
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(deprecationMsg)} */${newline}`);
8106
+ const deprecationComment = buildTokenDeprecationComment(token, "css");
8107
+ if (deprecationComment) {
8108
+ lines.push(`${indent}${deprecationComment}${newline}`);
7718
8109
  }
7719
- if (token.$description && token.$description !== "") {
7720
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(token.$description)} */${newline}`);
8110
+ const descriptionComment = buildTokenDescriptionComment(token, "css");
8111
+ if (descriptionComment) {
8112
+ lines.push(`${indent}${descriptionComment}${newline}`);
7721
8113
  }
7722
8114
  for (const entry of entries) {
7723
8115
  lines.push(`${indent}--${entry.name}:${space}${entry.value};${newline}`);
@@ -8357,6 +8749,7 @@ function cssRenderer() {
8357
8749
 
8358
8750
  // src/renderers/ios.ts
8359
8751
  init_utils();
8752
+ init_metadata();
8360
8753
  var toSRGB2 = culori.converter("rgb");
8361
8754
  var toP32 = culori.converter("p3");
8362
8755
  var SWIFT_TYPE_GROUP_MAP = {
@@ -8500,9 +8893,13 @@ var IosRenderer = class {
8500
8893
  const swiftValue = this.formatSwiftValue(token, options);
8501
8894
  const typeAnnotation = this.getTypeAnnotation(token);
8502
8895
  const annotation = typeAnnotation ? `: ${typeAnnotation}` : "";
8503
- const docComment = this.buildDocComment(token, indent);
8896
+ const docComment = buildTokenDescriptionComment(token, "swift");
8504
8897
  if (docComment) {
8505
- lines.push(docComment);
8898
+ lines.push(`${indent}${docComment}`);
8899
+ }
8900
+ const deprecationAttr = buildSwiftDeprecationAttribute(token);
8901
+ if (deprecationAttr) {
8902
+ lines.push(`${indent}${deprecationAttr}`);
8506
8903
  }
8507
8904
  lines.push(`${indent}${access3} ${staticPrefix}${swiftName}${annotation} = ${swiftValue}`);
8508
8905
  }
@@ -8517,15 +8914,6 @@ var IosRenderer = class {
8517
8914
  }
8518
8915
  return Array.from(imports).sort();
8519
8916
  }
8520
- /**
8521
- * Builds a `///` doc comment from a token's `$description`, if present.
8522
- */
8523
- buildDocComment(token, indent) {
8524
- if (!token.$description) {
8525
- return void 0;
8526
- }
8527
- return `${indent}/// ${token.$description}`;
8528
- }
8529
8917
  /**
8530
8918
  * Builds a qualified Swift name from a token's path, preserving parent
8531
8919
  * hierarchy segments to avoid duplicate identifiers.
@@ -8916,6 +9304,7 @@ function iosRenderer() {
8916
9304
  // src/renderers/js-module.ts
8917
9305
  init_utils();
8918
9306
  init_token_utils();
9307
+ init_metadata();
8919
9308
  var JsModuleRenderer = class {
8920
9309
  async format(context, options) {
8921
9310
  const opts = {
@@ -8953,17 +9342,10 @@ var JsModuleRenderer = class {
8953
9342
  return outputTree(files);
8954
9343
  }
8955
9344
  async formatTokens(tokens, options) {
8956
- const opts = {
8957
- preset: options.preset ?? "standalone",
8958
- structure: options.structure ?? "nested",
8959
- minify: options.minify ?? false,
8960
- moduleName: options.moduleName ?? "tokens",
8961
- generateHelper: options.generateHelper ?? false
8962
- };
8963
9345
  const lines = [];
8964
- lines.push(...this.formatAsObject(tokens, opts));
9346
+ lines.push(...this.formatAsObject(tokens, options));
8965
9347
  const code = lines.join("\n");
8966
- if (opts.minify) {
9348
+ if (options.minify) {
8967
9349
  return code;
8968
9350
  }
8969
9351
  return await prettier__default.default.format(code, {
@@ -8976,20 +9358,53 @@ var JsModuleRenderer = class {
8976
9358
  trailingComma: "es5"
8977
9359
  });
8978
9360
  }
8979
- /**
8980
- * Format as default export object
8981
- */
8982
9361
  formatAsObject(tokens, options) {
8983
9362
  const lines = [];
8984
- const tokenObj = this.tokensToPlainObject(tokens, options.structure);
9363
+ const tokenMap = this.buildTokenMap(tokens);
8985
9364
  const varName = options.moduleName !== "" ? options.moduleName : "tokens";
8986
- lines.push(`const ${varName} = {`);
8987
- this.addObjectProperties(lines, tokenObj, 1);
8988
- 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
+ }
8989
9375
  lines.push("");
8990
9376
  lines.push(`export default ${varName}`);
8991
9377
  return lines;
8992
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
+ }
8993
9408
  tokensToPlainObject(tokens, structure) {
8994
9409
  if (structure === "nested") {
8995
9410
  return buildNestedTokenObject(tokens, (token) => token.$value);
@@ -9000,7 +9415,7 @@ var JsModuleRenderer = class {
9000
9415
  }
9001
9416
  return result;
9002
9417
  }
9003
- addObjectProperties(lines, obj, indent) {
9418
+ addNestedProperties(lines, obj, tokenMap, indent) {
9004
9419
  const indentStr2 = " ".repeat(indent);
9005
9420
  const entries = Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
9006
9421
  for (let i = 0; i < entries.length; i++) {
@@ -9012,19 +9427,20 @@ var JsModuleRenderer = class {
9012
9427
  const isLast = i === entries.length - 1;
9013
9428
  const isNestedObject = typeof value === "object" && value !== null && !Array.isArray(value);
9014
9429
  if (!isNestedObject) {
9430
+ const token = tokenMap.get(key);
9431
+ if (token) {
9432
+ this.pushTokenComments(lines, token, indentStr2);
9433
+ }
9015
9434
  lines.push(
9016
9435
  `${indentStr2}${this.quoteKey(key)}: ${JSON.stringify(value)}${isLast ? "" : ","}`
9017
9436
  );
9018
9437
  continue;
9019
9438
  }
9020
9439
  lines.push(`${indentStr2}${this.quoteKey(key)}: {`);
9021
- this.addObjectProperties(lines, value, indent + 1);
9440
+ this.addNestedProperties(lines, value, tokenMap, indent + 1);
9022
9441
  lines.push(`${indentStr2}}${isLast ? "" : ","}`);
9023
9442
  }
9024
9443
  }
9025
- /**
9026
- * Quote key if necessary
9027
- */
9028
9444
  quoteKey(key) {
9029
9445
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
9030
9446
  return key;
@@ -9260,6 +9676,7 @@ function resolveOptions(options) {
9260
9676
 
9261
9677
  // src/renderers/tailwind.ts
9262
9678
  init_utils();
9679
+ init_metadata();
9263
9680
  var TAILWIND_NAMESPACE_MAP = {
9264
9681
  color: "color",
9265
9682
  dimension: "spacing",
@@ -9313,6 +9730,14 @@ var TailwindRenderer = class {
9313
9730
  for (const [, token] of getSortedTokenEntries(tokens)) {
9314
9731
  const varName = this.buildVariableName(token);
9315
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
+ }
9316
9741
  lines.push(`${indent}--${varName}:${space}${varValue};${newline}`);
9317
9742
  }
9318
9743
  lines.push(`}${newline}`);
@@ -9339,6 +9764,14 @@ var TailwindRenderer = class {
9339
9764
  for (const [, token] of getSortedTokenEntries(tokens)) {
9340
9765
  const varName = this.buildVariableName(token);
9341
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
+ }
9342
9775
  lines.push(`${tokenIndent}--${varName}:${space}${varValue};${newline}`);
9343
9776
  }
9344
9777
  if (hasMediaQuery) {
@@ -9611,16 +10044,26 @@ init_errors();
9611
10044
  * This source code is licensed under the MIT license found in the
9612
10045
  * LICENSE file in the root directory of this source tree.
9613
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
+ */
9614
10054
  /**
9615
10055
  * @license
9616
10056
  * Copyright (c) 2025 Dispersa Contributors
9617
10057
  * SPDX-License-Identifier: MIT
9618
10058
  */
9619
10059
 
9620
- exports.Dispersa = Dispersa;
9621
10060
  exports.android = android;
10061
+ exports.build = build;
10062
+ exports.buildOrThrow = buildOrThrow;
10063
+ exports.buildPermutation = buildPermutation;
9622
10064
  exports.css = css;
9623
10065
  exports.defineRenderer = defineRenderer;
10066
+ exports.generateTypes = generateTypes;
9624
10067
  exports.ios = ios;
9625
10068
  exports.isBorderToken = isBorderToken;
9626
10069
  exports.isColorToken = isColorToken;
@@ -9633,7 +10076,10 @@ exports.isTransitionToken = isTransitionToken;
9633
10076
  exports.isTypographyToken = isTypographyToken;
9634
10077
  exports.js = js;
9635
10078
  exports.json = json;
10079
+ exports.lint = lint;
9636
10080
  exports.outputTree = outputTree;
10081
+ exports.resolveAllPermutations = resolveAllPermutations;
10082
+ exports.resolveTokens = resolveTokens;
9637
10083
  exports.tailwind = tailwind;
9638
10084
  //# sourceMappingURL=index.cjs.map
9639
10085
  //# sourceMappingURL=index.cjs.map