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.js CHANGED
@@ -3,7 +3,12 @@ import addFormats from 'ajv-formats';
3
3
  import { constants } from 'fs';
4
4
  import { mkdir, writeFile, access, readFile } from 'fs/promises';
5
5
  import * as path from 'path';
6
+ import { isAbsolute, resolve } from 'path';
7
+ import { createRequire } from 'module';
8
+ import process2 from 'process';
9
+ import { createJiti } from 'jiti';
6
10
  import { JsonPointer } from 'json-ptr';
11
+ import { kebabCase } from 'change-case';
7
12
  import { converter, formatHex8, formatHex } from 'culori';
8
13
  import prettier from 'prettier';
9
14
 
@@ -18,7 +23,7 @@ var __export = (target, all) => {
18
23
  };
19
24
 
20
25
  // src/shared/errors/index.ts
21
- var DispersaError, TokenReferenceError, CircularReferenceError, ValidationError, FileOperationError, ConfigurationError, BasePermutationError, ModifierError;
26
+ var DispersaError, TokenReferenceError, CircularReferenceError, ValidationError, FileOperationError, ConfigurationError, BasePermutationError, ModifierError, LintError;
22
27
  var init_errors = __esm({
23
28
  "src/shared/errors/index.ts"() {
24
29
  DispersaError = class extends Error {
@@ -101,23 +106,19 @@ var init_errors = __esm({
101
106
  this.name = "ModifierError";
102
107
  }
103
108
  };
109
+ LintError = class extends DispersaError {
110
+ constructor(issues) {
111
+ const errorCount = issues.filter((i) => i.severity === "error").length;
112
+ const warningCount = issues.filter((i) => i.severity === "warn").length;
113
+ super(`Lint failed with ${errorCount} error(s) and ${warningCount} warning(s).`);
114
+ this.issues = issues;
115
+ this.name = "LintError";
116
+ }
117
+ };
104
118
  }
105
119
  });
106
120
 
107
121
  // src/shared/utils/token-utils.ts
108
- function formatDeprecationMessage(token, description = "", format = "bracket") {
109
- if (token.$deprecated == null || token.$deprecated === false) {
110
- return description;
111
- }
112
- const deprecationMsg = typeof token.$deprecated === "string" ? token.$deprecated : "";
113
- if (format === "comment") {
114
- const msg2 = deprecationMsg ? ` ${deprecationMsg}` : "";
115
- return `DEPRECATED${msg2}`;
116
- }
117
- const msg = deprecationMsg ? `: ${deprecationMsg}` : "";
118
- const prefix = `[DEPRECATED${msg}]`;
119
- return description ? `${prefix} ${description}` : prefix;
120
- }
121
122
  function stripInternalTokenMetadata(tokens) {
122
123
  const cleaned = {};
123
124
  for (const [name, token] of Object.entries(tokens)) {
@@ -2471,7 +2472,7 @@ var init_schemas = __esm({
2471
2472
  });
2472
2473
 
2473
2474
  // src/validation/config-schemas.ts
2474
- var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2475
+ var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, lintConfigSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2475
2476
  var init_config_schemas = __esm({
2476
2477
  "src/validation/config-schemas.ts"() {
2477
2478
  init_schemas();
@@ -2622,6 +2623,42 @@ var init_config_schemas = __esm({
2622
2623
  }
2623
2624
  }
2624
2625
  };
2626
+ lintConfigSchema = {
2627
+ $schema: "http://json-schema.org/draft-07/schema#",
2628
+ type: "object",
2629
+ properties: {
2630
+ enabled: {
2631
+ type: "boolean",
2632
+ description: "Enable linting (default: false, opt-in)"
2633
+ },
2634
+ failOnError: {
2635
+ type: "boolean",
2636
+ description: "Fail build on lint errors (default: true)"
2637
+ },
2638
+ plugins: {
2639
+ type: "object",
2640
+ description: "Plugins to load (by object or module path string)",
2641
+ additionalProperties: {
2642
+ oneOf: [{ type: "string" }, { type: "object" }]
2643
+ }
2644
+ },
2645
+ rules: {
2646
+ type: "object",
2647
+ description: "Rule configurations",
2648
+ additionalProperties: {
2649
+ oneOf: [
2650
+ { type: "string", enum: ["off", "warn", "error"] },
2651
+ {
2652
+ type: "array",
2653
+ minItems: 2,
2654
+ items: [{ type: "string", enum: ["off", "warn", "error"] }, { type: "object" }]
2655
+ }
2656
+ ]
2657
+ }
2658
+ }
2659
+ },
2660
+ additionalProperties: false
2661
+ };
2625
2662
  outputConfigSchema = {
2626
2663
  $schema: "http://json-schema.org/draft-07/schema#",
2627
2664
  type: "object",
@@ -2739,10 +2776,20 @@ var init_config_schemas = __esm({
2739
2776
  description: "Resolver configuration - file path or ResolverDocument object"
2740
2777
  },
2741
2778
  buildPath: { type: "string" },
2779
+ validation: {
2780
+ type: "object",
2781
+ properties: {
2782
+ mode: { type: "string", enum: ["error", "warn", "off"] }
2783
+ }
2784
+ },
2742
2785
  hooks: {
2743
2786
  type: "object",
2744
2787
  description: "Global build lifecycle hooks (functions, validated at runtime)",
2745
2788
  additionalProperties: true
2789
+ },
2790
+ lint: {
2791
+ ...lintConfigSchema,
2792
+ description: "Linting configuration"
2746
2793
  }
2747
2794
  },
2748
2795
  additionalProperties: false
@@ -3290,7 +3337,7 @@ function indentStr(width, level) {
3290
3337
  function buildGeneratedFileHeader() {
3291
3338
  return [
3292
3339
  "// Generated by Dispersa - do not edit manually",
3293
- "// https://github.com/timges/dispersa"
3340
+ "// https://github.com/dispersa-core/dispersa"
3294
3341
  ].join("\n");
3295
3342
  }
3296
3343
  function toSafeIdentifier(name, keywords, capitalize) {
@@ -3502,6 +3549,94 @@ var init_utils = __esm({
3502
3549
  }
3503
3550
  });
3504
3551
 
3552
+ // src/renderers/metadata.ts
3553
+ function sanitizeText(text, format) {
3554
+ switch (format) {
3555
+ case "css":
3556
+ case "tailwind":
3557
+ return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
3558
+ case "kotlin":
3559
+ return text.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
3560
+ case "js":
3561
+ case "swift":
3562
+ return text.replace(/\r?\n/g, " ").trim();
3563
+ default:
3564
+ return text.trim();
3565
+ }
3566
+ }
3567
+ function buildDeprecationText(token) {
3568
+ if (token.$deprecated == null || token.$deprecated === false) {
3569
+ return "";
3570
+ }
3571
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3572
+ return msg ? `DEPRECATED: ${msg}` : "DEPRECATED";
3573
+ }
3574
+ function buildTokenDescriptionComment(token, format) {
3575
+ if (!token.$description || token.$description === "") {
3576
+ return void 0;
3577
+ }
3578
+ const text = sanitizeText(token.$description, format);
3579
+ switch (format) {
3580
+ case "css":
3581
+ case "tailwind":
3582
+ return `/* ${text} */`;
3583
+ case "js":
3584
+ return `// ${text}`;
3585
+ case "swift":
3586
+ return `/// ${text}`;
3587
+ case "kotlin":
3588
+ return `/** ${text} */`;
3589
+ default:
3590
+ return void 0;
3591
+ }
3592
+ }
3593
+ function buildTokenDeprecationComment(token, format) {
3594
+ const text = buildDeprecationText(token);
3595
+ if (!text) {
3596
+ return void 0;
3597
+ }
3598
+ switch (format) {
3599
+ case "css":
3600
+ case "tailwind":
3601
+ return `/* ${text} */`;
3602
+ case "js":
3603
+ return `// ${text}`;
3604
+ case "swift":
3605
+ return `/// ${text}`;
3606
+ case "kotlin":
3607
+ return `/** ${text} */`;
3608
+ default:
3609
+ return void 0;
3610
+ }
3611
+ }
3612
+ function buildModifierComment(modifier, context) {
3613
+ return `/* Modifier: ${modifier}=${context} */`;
3614
+ }
3615
+ function buildSwiftDeprecationAttribute(token) {
3616
+ if (token.$deprecated == null || token.$deprecated === false) {
3617
+ return void 0;
3618
+ }
3619
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3620
+ if (msg) {
3621
+ return `@available(*, deprecated, message: "${sanitizeText(msg, "swift")}")`;
3622
+ }
3623
+ return "@available(*, deprecated)";
3624
+ }
3625
+ function buildKotlinDeprecationAnnotation(token) {
3626
+ if (token.$deprecated == null || token.$deprecated === false) {
3627
+ return void 0;
3628
+ }
3629
+ const msg = typeof token.$deprecated === "string" ? token.$deprecated : "";
3630
+ if (msg) {
3631
+ return `@Deprecated(message = "${sanitizeText(msg, "kotlin")}")`;
3632
+ }
3633
+ return "@Deprecated";
3634
+ }
3635
+ var init_metadata = __esm({
3636
+ "src/renderers/metadata.ts"() {
3637
+ }
3638
+ });
3639
+
3505
3640
  // src/renderers/bundlers/js.ts
3506
3641
  var js_exports = {};
3507
3642
  __export(js_exports, {
@@ -3606,7 +3741,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3606
3741
  }
3607
3742
  const metadata = buildMetadata(resolver);
3608
3743
  const jsBlocks = [];
3609
- for (const { tokens, modifierInputs } of bundleData) {
3744
+ for (const { tokens, modifierInputs, isBase } of bundleData) {
3610
3745
  const cleanTokens = stripInternalMetadata(tokens);
3611
3746
  const key = buildStableDashKey({
3612
3747
  modifierInputs,
@@ -3614,10 +3749,30 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3614
3749
  defaults: metadata.defaults
3615
3750
  });
3616
3751
  const camelKey = toCamelKey(key);
3752
+ const normalizedInputs = normalizeModifierInputs(modifierInputs);
3753
+ const modifierParts = [];
3754
+ for (const dim of metadata.dimensions) {
3755
+ const value = normalizedInputs[dim.toLowerCase()];
3756
+ if (value) {
3757
+ modifierParts.push(`${dim}=${value}`);
3758
+ }
3759
+ }
3617
3760
  const formattedJs = await formatTokens(cleanTokens);
3618
3761
  const tokenObject = extractObjectFromJsModule(formattedJs);
3619
3762
  const indentedObject = tokenObject.replace(/\n/g, "\n ");
3620
- jsBlocks.push(` // ${key}
3763
+ let comment;
3764
+ if (modifierParts.length > 0) {
3765
+ const modifierPart = modifierParts[0];
3766
+ const eqIndex = modifierPart.indexOf("=");
3767
+ const modifier = modifierPart.slice(0, eqIndex);
3768
+ const context = modifierPart.slice(eqIndex + 1);
3769
+ comment = buildModifierComment(modifier, context);
3770
+ } else if (isBase) {
3771
+ comment = "// Base permutation";
3772
+ } else {
3773
+ comment = `// ${key}`;
3774
+ }
3775
+ jsBlocks.push(` ${comment}
3621
3776
  ${JSON.stringify(camelKey)}: ${indentedObject}`);
3622
3777
  }
3623
3778
  return assembleJsBundle(metadata, jsBlocks, options?.generateHelper ?? false);
@@ -3625,6 +3780,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3625
3780
  var init_js = __esm({
3626
3781
  "src/renderers/bundlers/js.ts"() {
3627
3782
  init_errors();
3783
+ init_metadata();
3628
3784
  init_utils();
3629
3785
  }
3630
3786
  });
@@ -3994,7 +4150,8 @@ var BuildOrchestrator = class {
3994
4150
  resolver,
3995
4151
  config.transforms,
3996
4152
  config.preprocessors,
3997
- config.filters
4153
+ config.filters,
4154
+ config.lint
3998
4155
  );
3999
4156
  return this.executeBuild(buildPath, config, permutations2, resolver);
4000
4157
  }
@@ -4005,7 +4162,8 @@ var BuildOrchestrator = class {
4005
4162
  modifierInputs,
4006
4163
  config.transforms,
4007
4164
  config.preprocessors,
4008
- config.filters
4165
+ config.filters,
4166
+ config.lint
4009
4167
  );
4010
4168
  return { tokens, modifierInputs: resolvedInputs };
4011
4169
  })
@@ -4266,7 +4424,358 @@ var OutputProcessor = class {
4266
4424
 
4267
4425
  // src/build/pipeline/token-pipeline.ts
4268
4426
  init_resolver_loader();
4269
- init_validation_handler();
4427
+
4428
+ // src/lint/plugin-loader.ts
4429
+ init_errors();
4430
+ var PluginLoader = class {
4431
+ cwd;
4432
+ jiti = null;
4433
+ cache = /* @__PURE__ */ new Map();
4434
+ constructor(options = {}) {
4435
+ this.cwd = options.cwd ?? process2.cwd();
4436
+ }
4437
+ /**
4438
+ * Load a plugin from an inline object or module path
4439
+ *
4440
+ * @param source - Plugin object or module path string
4441
+ * @returns Loaded plugin
4442
+ * @throws {ConfigurationError} If plugin cannot be loaded or is invalid
4443
+ */
4444
+ async load(source) {
4445
+ if (this.isPluginObject(source)) {
4446
+ this.validatePlugin(source);
4447
+ return source;
4448
+ }
4449
+ const modulePath = source;
4450
+ const cached = this.cache.get(modulePath);
4451
+ if (cached) {
4452
+ return cached;
4453
+ }
4454
+ const plugin = await this.loadFromModule(modulePath);
4455
+ this.validatePlugin(plugin);
4456
+ this.cache.set(modulePath, plugin);
4457
+ return plugin;
4458
+ }
4459
+ /**
4460
+ * Load multiple plugins
4461
+ *
4462
+ * @param plugins - Record of namespace to plugin source
4463
+ * @returns Record of namespace to loaded plugin
4464
+ */
4465
+ async loadAll(plugins) {
4466
+ const entries = Object.entries(plugins);
4467
+ const loaded = await Promise.all(
4468
+ entries.map(async ([namespace, source]) => [namespace, await this.load(source)])
4469
+ );
4470
+ return Object.fromEntries(loaded);
4471
+ }
4472
+ /**
4473
+ * Check if source is an inline plugin object
4474
+ */
4475
+ isPluginObject(source) {
4476
+ return typeof source !== "string";
4477
+ }
4478
+ /**
4479
+ * Load a plugin from a module path
4480
+ */
4481
+ async loadFromModule(modulePath) {
4482
+ const resolvedPath = isAbsolute(modulePath) ? modulePath : resolve(this.cwd, modulePath);
4483
+ const isFilePath = modulePath.startsWith("./") || modulePath.startsWith("../") || isAbsolute(modulePath);
4484
+ try {
4485
+ if (isFilePath) {
4486
+ return await this.loadFromFile(resolvedPath);
4487
+ }
4488
+ return await this.loadFromPackage(modulePath);
4489
+ } catch (error) {
4490
+ const message = error instanceof Error ? error.message : String(error);
4491
+ throw new ConfigurationError(`Failed to load lint plugin '${modulePath}': ${message}`);
4492
+ }
4493
+ }
4494
+ /**
4495
+ * Load a plugin from a file path using jiti (supports TypeScript)
4496
+ */
4497
+ async loadFromFile(filePath) {
4498
+ this.jiti ??= createJiti(this.cwd, {
4499
+ interopDefault: true
4500
+ });
4501
+ const loaded = await this.jiti(filePath);
4502
+ const plugin = this.extractPlugin(loaded);
4503
+ if (!plugin) {
4504
+ throw new ConfigurationError(`Plugin file '${filePath}' does not export a valid LintPlugin`);
4505
+ }
4506
+ return plugin;
4507
+ }
4508
+ /**
4509
+ * Load a plugin from a package name
4510
+ */
4511
+ async loadFromPackage(packageName) {
4512
+ const require2 = createRequire(this.cwd);
4513
+ let resolvedPath;
4514
+ try {
4515
+ resolvedPath = require2.resolve(packageName, { paths: [this.cwd] });
4516
+ } catch {
4517
+ try {
4518
+ resolvedPath = require2.resolve(packageName);
4519
+ } catch {
4520
+ throw new ConfigurationError(
4521
+ `Cannot find package '${packageName}'. Make sure it is installed.`
4522
+ );
4523
+ }
4524
+ }
4525
+ this.jiti ??= createJiti(this.cwd, {
4526
+ interopDefault: true
4527
+ });
4528
+ const loaded = await this.jiti(resolvedPath);
4529
+ const plugin = this.extractPlugin(loaded);
4530
+ if (!plugin) {
4531
+ throw new ConfigurationError(`Package '${packageName}' does not export a valid LintPlugin`);
4532
+ }
4533
+ return plugin;
4534
+ }
4535
+ /**
4536
+ * Extract plugin from loaded module
4537
+ *
4538
+ * Supports multiple export patterns:
4539
+ * - export default plugin
4540
+ * - export const plugin = {...}
4541
+ * - module.exports = plugin (CJS)
4542
+ */
4543
+ extractPlugin(loaded) {
4544
+ if (!loaded || typeof loaded !== "object") {
4545
+ return null;
4546
+ }
4547
+ const module = loaded;
4548
+ if (module.default && this.isValidPluginStructure(module.default)) {
4549
+ return module.default;
4550
+ }
4551
+ if (module.plugin && this.isValidPluginStructure(module.plugin)) {
4552
+ return module.plugin;
4553
+ }
4554
+ if (this.isValidPluginStructure(loaded)) {
4555
+ return loaded;
4556
+ }
4557
+ return null;
4558
+ }
4559
+ /**
4560
+ * Check if object has required plugin structure
4561
+ */
4562
+ isValidPluginStructure(obj) {
4563
+ if (!obj || typeof obj !== "object") {
4564
+ return false;
4565
+ }
4566
+ const plugin = obj;
4567
+ const rules = plugin.rules;
4568
+ if (plugin.meta === void 0 || typeof plugin.meta !== "object" || !plugin.meta.name || rules === void 0 || Object.keys(rules).length === 0) {
4569
+ return false;
4570
+ }
4571
+ return true;
4572
+ }
4573
+ /**
4574
+ * Validate a loaded plugin
4575
+ */
4576
+ validatePlugin(plugin) {
4577
+ if (!plugin.meta) {
4578
+ throw new ConfigurationError("Lint plugin must have a meta property with name");
4579
+ }
4580
+ if (!plugin.meta.name) {
4581
+ throw new ConfigurationError("Lint plugin meta.name is required");
4582
+ }
4583
+ if (!plugin.rules || typeof plugin.rules !== "object" || Object.keys(plugin.rules).length === 0) {
4584
+ throw new ConfigurationError(
4585
+ `Lint plugin '${plugin.meta.name}' must have a non-empty rules object`
4586
+ );
4587
+ }
4588
+ for (const [ruleName, rule] of Object.entries(plugin.rules)) {
4589
+ if (!rule.meta) {
4590
+ throw new ConfigurationError(
4591
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta property`
4592
+ );
4593
+ }
4594
+ if (!rule.meta.messages || typeof rule.meta.messages !== "object") {
4595
+ throw new ConfigurationError(
4596
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing meta.messages`
4597
+ );
4598
+ }
4599
+ if (typeof rule.create !== "function") {
4600
+ throw new ConfigurationError(
4601
+ `Rule '${ruleName}' in plugin '${plugin.meta.name}' is missing create function`
4602
+ );
4603
+ }
4604
+ }
4605
+ }
4606
+ /**
4607
+ * Clear the plugin cache
4608
+ */
4609
+ clearCache() {
4610
+ this.cache.clear();
4611
+ }
4612
+ };
4613
+
4614
+ // src/lint/lint-runner.ts
4615
+ var LintRunner = class {
4616
+ config;
4617
+ pluginLoader;
4618
+ resolvedConfig = null;
4619
+ warn;
4620
+ constructor(config) {
4621
+ this.config = config;
4622
+ this.pluginLoader = new PluginLoader();
4623
+ this.warn = config.onWarn ?? console.warn;
4624
+ }
4625
+ /**
4626
+ * Run all configured rules against the provided tokens
4627
+ *
4628
+ * Rules are executed in parallel for performance. Issues are collected
4629
+ * and returned with counts by severity.
4630
+ *
4631
+ * @param tokens - Resolved tokens to lint
4632
+ * @returns Lint result with issues and counts
4633
+ */
4634
+ async run(tokens) {
4635
+ this.resolvedConfig ??= await this.resolveConfig();
4636
+ const { rules, plugins } = this.resolvedConfig;
4637
+ const issues = [];
4638
+ if (Object.keys(rules).length === 0) {
4639
+ return { issues: [], errorCount: 0, warningCount: 0 };
4640
+ }
4641
+ const rulePromises = Object.entries(rules).map(async ([ruleId, ruleConfig]) => {
4642
+ const { severity, options } = ruleConfig;
4643
+ const rule = this.resolveRule(ruleId, plugins);
4644
+ if (!rule) {
4645
+ this.warn(`[lint] Unknown rule '${ruleId}' - no plugin provides this rule`);
4646
+ return [];
4647
+ }
4648
+ const reports = [];
4649
+ const applicableTokens = this.filterTokensByAppliesTo(tokens, rule.meta.appliesTo);
4650
+ const mergedOptions = rule.defaultOptions ? { ...rule.defaultOptions, ...options } : options;
4651
+ const context = {
4652
+ id: ruleId,
4653
+ options: mergedOptions,
4654
+ tokens: applicableTokens,
4655
+ report: (descriptor) => {
4656
+ reports.push(descriptor);
4657
+ }
4658
+ };
4659
+ try {
4660
+ await rule.create(context);
4661
+ } catch (error) {
4662
+ const message = error instanceof Error ? error.message : String(error);
4663
+ return [
4664
+ {
4665
+ ruleId: "lint/rule-error",
4666
+ severity: "error",
4667
+ message: `Rule '${ruleId}' failed: ${message}`,
4668
+ tokenName: "(rule execution)",
4669
+ tokenPath: []
4670
+ }
4671
+ ];
4672
+ }
4673
+ return reports.map((report) => {
4674
+ const messageTemplate = rule.meta.messages[report.messageId];
4675
+ const message = messageTemplate ? this.interpolateMessage(messageTemplate, report.data) : report.messageId;
4676
+ return {
4677
+ ruleId,
4678
+ severity,
4679
+ message,
4680
+ tokenName: report.token.name,
4681
+ tokenPath: report.token.path
4682
+ };
4683
+ });
4684
+ });
4685
+ const allIssues = await Promise.all(rulePromises);
4686
+ issues.push(...allIssues.flat());
4687
+ const errorCount = issues.filter((i) => i.severity === "error").length;
4688
+ const warningCount = issues.filter((i) => i.severity === "warn").length;
4689
+ return { issues, errorCount, warningCount };
4690
+ }
4691
+ /**
4692
+ * Resolve configuration: load plugins, parse rule configs
4693
+ */
4694
+ async resolveConfig() {
4695
+ const { plugins: pluginSources = {}, rules: ruleConfigs = {} } = this.config;
4696
+ const plugins = await this.pluginLoader.loadAll(pluginSources);
4697
+ const rules = {};
4698
+ for (const [ruleId, config] of Object.entries(ruleConfigs)) {
4699
+ const resolved = this.resolveRuleConfig(config);
4700
+ if (resolved) {
4701
+ rules[ruleId] = resolved;
4702
+ }
4703
+ }
4704
+ return {
4705
+ enabled: true,
4706
+ failOnError: this.config.failOnError ?? true,
4707
+ plugins,
4708
+ rules
4709
+ };
4710
+ }
4711
+ filterTokensByAppliesTo(tokens, appliesTo) {
4712
+ if (!appliesTo || appliesTo === "all") {
4713
+ return tokens;
4714
+ }
4715
+ const filtered = {};
4716
+ for (const [name, token] of Object.entries(tokens)) {
4717
+ if (token.$type && appliesTo.includes(token.$type)) {
4718
+ filtered[name] = token;
4719
+ }
4720
+ }
4721
+ return filtered;
4722
+ }
4723
+ /**
4724
+ * Parse rule configuration into resolved format
4725
+ */
4726
+ resolveRuleConfig(config) {
4727
+ if (typeof config === "string") {
4728
+ if (config === "off") {
4729
+ return null;
4730
+ }
4731
+ return { severity: config, options: {} };
4732
+ }
4733
+ const [severity, options = {}] = config;
4734
+ if (severity === "off") {
4735
+ return null;
4736
+ }
4737
+ return { severity, options };
4738
+ }
4739
+ /**
4740
+ * Resolve a rule from plugins by rule ID
4741
+ *
4742
+ * Rule IDs are formatted as 'namespace/rule-name'
4743
+ */
4744
+ resolveRule(ruleId, plugins) {
4745
+ const separatorIndex = ruleId.indexOf("/");
4746
+ if (separatorIndex === -1) {
4747
+ return null;
4748
+ }
4749
+ const namespace = ruleId.slice(0, separatorIndex);
4750
+ const ruleName = ruleId.slice(separatorIndex + 1);
4751
+ const plugin = plugins[namespace];
4752
+ if (!plugin) {
4753
+ return null;
4754
+ }
4755
+ return plugin.rules[ruleName] ?? null;
4756
+ }
4757
+ /**
4758
+ * Interpolate message template with data
4759
+ *
4760
+ * Replaces {{key}} placeholders with values from data
4761
+ */
4762
+ interpolateMessage(template, data) {
4763
+ if (!data) {
4764
+ return template;
4765
+ }
4766
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
4767
+ const value = data[key];
4768
+ return value !== void 0 ? String(value) : `{{${key}}}`;
4769
+ });
4770
+ }
4771
+ /**
4772
+ * Clear the plugin cache
4773
+ */
4774
+ clearCache() {
4775
+ this.pluginLoader.clearCache();
4776
+ this.resolvedConfig = null;
4777
+ }
4778
+ };
4270
4779
 
4271
4780
  // src/shared/constants.ts
4272
4781
  var DEFAULT_MAX_ALIAS_DEPTH = 10;
@@ -5461,6 +5970,10 @@ var ResolutionEngine = class {
5461
5970
  return typeof obj === "object" && obj !== null && "contexts" in obj && typeof obj.contexts === "object";
5462
5971
  }
5463
5972
  };
5973
+
5974
+ // src/build/pipeline/token-pipeline.ts
5975
+ init_errors();
5976
+ init_validation_handler();
5464
5977
  init_errors();
5465
5978
 
5466
5979
  // src/shared/utils/path-utils.ts
@@ -5953,6 +6466,8 @@ var TokenPipeline = class {
5953
6466
  resolverLoader;
5954
6467
  tokenParser;
5955
6468
  aliasResolver;
6469
+ lintRunner = null;
6470
+ lintConfigCache = null;
5956
6471
  constructor(options = {}) {
5957
6472
  this.options = options;
5958
6473
  this.validationHandler = new ValidationHandler(options.validation);
@@ -5972,8 +6487,9 @@ var TokenPipeline = class {
5972
6487
  * 6. Parse and flatten token structure
5973
6488
  * 7. Resolve alias references
5974
6489
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
5975
- * 9. Apply filters (if provided) — runs first to remove tokens before transforms
5976
- * 10. Apply transforms (if provided) — runs on the already-filtered token set
6490
+ * 9. Run lint rules (if enabled)
6491
+ * 10. Apply filters (if provided) — runs first to remove tokens before transforms
6492
+ * 11. Apply transforms (if provided) — runs on the already-filtered token set
5977
6493
  *
5978
6494
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
5979
6495
  *
@@ -5982,9 +6498,11 @@ var TokenPipeline = class {
5982
6498
  * @param transformList - Optional transforms to apply
5983
6499
  * @param preprocessorList - Optional preprocessors to apply
5984
6500
  * @param filterList - Optional filters to apply before transforms
5985
- * @returns Final tokens and resolution engine
6501
+ * @param lintConfig - Optional lint configuration for this run
6502
+ * @returns Final tokens, resolution engine, and lint result
5986
6503
  */
5987
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6504
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6505
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
5988
6506
  const stage1 = await this.loadResolver(resolver);
5989
6507
  const engine = this.createEngine(stage1);
5990
6508
  const result = await this.runPipelineStages(
@@ -5992,29 +6510,32 @@ var TokenPipeline = class {
5992
6510
  modifierInputs,
5993
6511
  preprocessorList,
5994
6512
  filterList,
5995
- transformList
6513
+ transformList,
6514
+ effectiveLintConfig
5996
6515
  );
5997
6516
  return {
5998
6517
  tokens: result.tokens,
5999
6518
  resolutionEngine: result.resolutionEngine,
6000
- modifierInputs: result.modifierInputs
6519
+ modifierInputs: result.modifierInputs,
6520
+ lintResult: result.lintResult
6001
6521
  };
6002
6522
  }
6003
6523
  /**
6004
- * Run pipeline stages 3-9 on a pre-created engine
6524
+ * Run pipeline stages 3-11 on a pre-created engine
6005
6525
  *
6006
6526
  * Shared by both `resolve()` (single permutation) and
6007
6527
  * `resolveAllPermutations()` (parallel permutations) to keep the
6008
6528
  * stage sequence defined in exactly one place.
6009
6529
  */
6010
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6530
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6011
6531
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6012
6532
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6013
6533
  const refResolved = await this.resolveReferences(preprocessed);
6014
6534
  const flattened = this.flattenTokens(refResolved);
6015
6535
  const aliasResolved = this.resolveAliases(flattened);
6016
6536
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6017
- const filtered = this.applyFilterStage(rootStripped, filterList);
6537
+ const linted = await this.runLintStage(rootStripped, lintConfig);
6538
+ const filtered = this.applyFilterStage(linted, filterList);
6018
6539
  return this.applyTransformStage(filtered, transformList);
6019
6540
  }
6020
6541
  /**
@@ -6127,6 +6648,47 @@ var TokenPipeline = class {
6127
6648
  }
6128
6649
  return { ...stage, aliasResolvedTokens: result };
6129
6650
  }
6651
+ /**
6652
+ * Stage 9: Run lint rules against tokens
6653
+ *
6654
+ * Linting runs after alias resolution and $root stripping but before
6655
+ * filters and transforms. This ensures rules see the full token set
6656
+ * with resolved values.
6657
+ */
6658
+ async runLintStage(stage, lintConfig) {
6659
+ if (!lintConfig?.enabled) {
6660
+ return stage;
6661
+ }
6662
+ if (!this.lintRunner || !this.isLintConfigEqual(this.lintConfigCache, lintConfig)) {
6663
+ this.lintRunner = new LintRunner({
6664
+ plugins: lintConfig.plugins,
6665
+ rules: lintConfig.rules,
6666
+ onWarn: (msg) => this.validationHandler.warn(msg)
6667
+ });
6668
+ this.lintConfigCache = lintConfig;
6669
+ }
6670
+ const lintResult = await this.lintRunner.run(stage.aliasResolvedTokens);
6671
+ if (lintResult.errorCount > 0 && lintConfig.failOnError !== false) {
6672
+ throw new LintError(lintResult.issues);
6673
+ }
6674
+ for (const issue of lintResult.issues.filter((i) => i.severity === "warn")) {
6675
+ this.validationHandler.warn(
6676
+ `[${issue.ruleId}] ${issue.message} (token: ${issue.tokenName})`
6677
+ );
6678
+ }
6679
+ return { ...stage, lintResult };
6680
+ }
6681
+ isLintConfigEqual(a, b) {
6682
+ if (!a || !b) return false;
6683
+ if (a.enabled !== b.enabled) return false;
6684
+ if (a.failOnError !== b.failOnError) return false;
6685
+ if (a.plugins !== b.plugins) return false;
6686
+ if (a.rules !== b.rules) return false;
6687
+ return true;
6688
+ }
6689
+ /**
6690
+ * Stage 10: Apply filters to the linted token set
6691
+ */
6130
6692
  applyFilterStage(stage, filterList) {
6131
6693
  let tokens = stage.aliasResolvedTokens;
6132
6694
  if (filterList !== void 0 && filterList.length > 0) {
@@ -6135,7 +6697,7 @@ var TokenPipeline = class {
6135
6697
  return { ...stage, aliasResolvedTokens: tokens };
6136
6698
  }
6137
6699
  /**
6138
- * Stage 9: Apply transforms to the filtered token set
6700
+ * Stage 11: Apply transforms to the filtered token set
6139
6701
  */
6140
6702
  applyTransformStage(stage, transformList) {
6141
6703
  let tokens = stage.aliasResolvedTokens;
@@ -6169,8 +6731,10 @@ var TokenPipeline = class {
6169
6731
  * @param transformList - Optional transforms to apply
6170
6732
  * @param preprocessorList - Optional preprocessors to apply
6171
6733
  * @param filterList - Optional filters to apply before transforms
6734
+ * @param lintConfig - Optional lint configuration for this run
6172
6735
  */
6173
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6736
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6737
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6174
6738
  const stage1 = await this.loadResolver(resolver);
6175
6739
  const discoveryEngine = this.createEngine(stage1);
6176
6740
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6183,11 +6747,13 @@ var TokenPipeline = class {
6183
6747
  modifierInputs,
6184
6748
  preprocessorList,
6185
6749
  filterList,
6186
- transformList
6750
+ transformList,
6751
+ effectiveLintConfig
6187
6752
  );
6188
6753
  return {
6189
6754
  tokens: result.tokens,
6190
- modifierInputs: result.modifierInputs
6755
+ modifierInputs: result.modifierInputs,
6756
+ lintResult: result.lintResult
6191
6757
  };
6192
6758
  })
6193
6759
  );
@@ -6208,286 +6774,106 @@ var TokenPipeline = class {
6208
6774
  init_errors();
6209
6775
  init_token_utils();
6210
6776
  init_validator();
6211
- var Dispersa = class {
6212
- validator;
6213
- pipeline;
6214
- outputProcessor;
6215
- orchestrator;
6216
- options;
6217
- /**
6218
- * Creates a new Dispersa instance
6219
- *
6220
- * @param options - Configuration options (optional, can be empty object)
6221
- * @param options.resolver - Default resolver (file path or inline object, optional if provided at build time)
6222
- * @param options.buildPath - Default output directory for generated files (omit for in-memory mode)
6223
- * @throws {ConfigurationError} If options are invalid and validation is enabled
6224
- */
6225
- constructor(options = {}) {
6226
- this.options = options;
6227
- this.validator = new SchemaValidator();
6228
- const errors = this.validator.validateDispersaOptions(options);
6229
- if (errors.length > 0) {
6230
- throw new ConfigurationError(
6231
- `Invalid Dispersa options: ${this.validator.getErrorMessage(errors)}`
6232
- );
6233
- }
6234
- this.pipeline = new TokenPipeline({ validation: options.validation });
6235
- this.outputProcessor = new OutputProcessor();
6236
- this.orchestrator = new BuildOrchestrator(this.pipeline, this.outputProcessor);
6237
- }
6238
- /**
6239
- * Builds design tokens from a resolver configuration
6240
- *
6241
- * Processes tokens through the resolution pipeline, applies preprocessors,
6242
- * transforms, and filters, then generates output files in multiple formats
6243
- * for specified outputs.
6244
- *
6245
- * **Runtime Validation:**
6246
- * This method validates the build configuration
6247
- * and all output configurations before processing, throwing helpful errors for
6248
- * invalid inputs.
6249
- *
6250
- * **Permutation Handling:**
6251
- * - If `config.permutations` is provided and non-empty, builds those specific permutations
6252
- * - If `config.permutations` is undefined or empty, auto-discovers all permutations from resolver
6253
- *
6254
- * @param config - Build configuration
6255
- * @param config.resolver - Resolver configuration (file path or inline object, optional if set in constructor)
6256
- * @param config.outputs - Array of output configurations (renderers, transforms, filters)
6257
- * @param config.buildPath - Output directory for generated files (omit for in-memory mode, optional if set in constructor)
6258
- * @param config.transforms - Global transforms to apply to all tokens
6259
- * @param config.preprocessors - Global preprocessors to apply before parsing
6260
- * @param config.permutations - Array of modifier inputs for generating variations
6261
- * @returns Build result with success status and generated output files
6262
- * @throws {ConfigurationError} If configuration is invalid
6263
- * @throws {FileOperationError} If file operations fail
6264
- *
6265
- * @example Basic build
6266
- * ```typescript
6267
- * const dispersa = new Dispersa({
6268
- * resolver: './tokens.resolver.json',
6269
- * buildPath: './output'
6270
- * })
6271
- *
6272
- * const result = await dispersa.build({
6273
- * outputs: [
6274
- * css({
6275
- * name: 'css',
6276
- * file: 'tokens.css',
6277
- * preset: 'bundle',
6278
- * selector: ':root',
6279
- * transforms: [nameKebabCase()]
6280
- * })
6281
- * ]
6282
- * })
6283
- * ```
6284
- *
6285
- * @example With filters and multiple presets
6286
- * ```typescript
6287
- * const result = await dispersa.build({
6288
- * resolver: './tokens.resolver.json',
6289
- * outputs: [
6290
- * css({
6291
- * name: 'css',
6292
- * file: 'tokens.css',
6293
- * preset: 'bundle',
6294
- * selector: ':root', // All themes in one file
6295
- * transforms: [nameKebabCase()]
6296
- * }),
6297
- * json({
6298
- * name: 'json',
6299
- * file: 'tokens-{theme}.json', // Separate file per theme
6300
- * preset: 'standalone',
6301
- * structure: 'flat'
6302
- * })
6303
- * ],
6304
- * buildPath: './output',
6305
- * permutations: [
6306
- * { theme: 'light' },
6307
- * { theme: 'dark' }
6308
- * ]
6309
- * })
6310
- * ```
6311
- */
6312
- async build(config) {
6313
- try {
6314
- return await this.buildOrThrow(config);
6315
- } catch (error) {
6316
- return {
6317
- success: false,
6318
- outputs: [],
6319
- errors: [toBuildError(error)]
6320
- };
6321
- }
6322
- }
6323
- /**
6324
- * Builds design tokens and throws on any failure.
6325
- *
6326
- * Unlike {@link build}, which catches errors and returns them inside
6327
- * `BuildResult.errors`, this method propagates the first error as an
6328
- * exception. Use it when you want fail-fast behavior in CLI or CI workflows.
6329
- *
6330
- * @param config - Build configuration specifying resolver, outputs, transforms, etc.
6331
- * @returns A successful `BuildResult` (never contains errors)
6332
- * @throws {ConfigurationError} When the build config or resolver is invalid
6333
- * @throws {DispersaError} When token resolution, transforms, or rendering fails
6334
- *
6335
- * @example
6336
- * ```typescript
6337
- * try {
6338
- * const result = await dispersa.buildOrThrow({
6339
- * resolver: './tokens.resolver.json',
6340
- * outputs: [css({ name: 'css', file: 'tokens.css' })],
6341
- * buildPath: './output',
6342
- * })
6343
- * } catch (error) {
6344
- * process.exit(1)
6345
- * }
6346
- * ```
6347
- */
6348
- async buildOrThrow(config) {
6349
- const configErrors = this.validator.validateBuildConfig(config);
6350
- if (configErrors.length > 0) {
6351
- throw new ConfigurationError(
6352
- `Invalid build configuration: ${this.validator.getErrorMessage(configErrors)}`
6353
- );
6354
- }
6355
- for (const output of config.outputs) {
6356
- const outputErrors = this.validator.validateOutputConfig(output);
6357
- if (outputErrors.length > 0) {
6358
- throw new ConfigurationError(
6359
- `Invalid output '${output.name}': ${this.validator.getErrorMessage(outputErrors)}`
6360
- );
6361
- }
6362
- }
6363
- const { resolver, buildPath } = this.resolveConfig(config);
6364
- return this.orchestrator.build(resolver, buildPath, config);
6777
+ function createValidator() {
6778
+ return new SchemaValidator();
6779
+ }
6780
+ function createPipeline(options) {
6781
+ return new TokenPipeline({ validation: options?.validation });
6782
+ }
6783
+ function createOutputProcessor() {
6784
+ return new OutputProcessor();
6785
+ }
6786
+ function createOrchestrator(pipeline, outputProcessor) {
6787
+ return new BuildOrchestrator(pipeline, outputProcessor);
6788
+ }
6789
+ function resolveConfig(config, options) {
6790
+ const resolver = config.resolver ?? options?.resolver;
6791
+ const buildPath = config.buildPath ?? options?.buildPath ?? "";
6792
+ if (!resolver) {
6793
+ throw new ConfigurationError("resolver is required in build config");
6365
6794
  }
6366
- /**
6367
- * Builds tokens for a single permutation with all configured outputs
6368
- *
6369
- * Convenience wrapper around `build()` that forces a single permutation.
6370
- * This means it has the same runtime validation and error semantics as `build()`:
6371
- * it returns a `BuildResult` object rather than throwing (use `buildOrThrow()` if you
6372
- * want fail-fast behavior).
6373
- *
6374
- * @param config - Build configuration
6375
- * @param modifierInputs - Modifier values (e.g., `{ theme: 'dark' }`)
6376
- * @returns Build result (success, outputs, optional errors)
6377
- */
6378
- async buildPermutation(config, modifierInputs = {}) {
6379
- return await this.build({
6380
- ...config,
6381
- permutations: [modifierInputs]
6382
- });
6795
+ return { resolver, buildPath };
6796
+ }
6797
+ function validateBuildConfig(validator, config) {
6798
+ const configErrors = validator.validateBuildConfig(config);
6799
+ if (configErrors.length > 0) {
6800
+ throw new ConfigurationError(
6801
+ `Invalid build configuration: ${validator.getErrorMessage(configErrors)}`
6802
+ );
6383
6803
  }
6384
- /**
6385
- * Resolve configuration with constructor defaults
6386
- */
6387
- resolveConfig(config) {
6388
- const resolver = config.resolver ?? this.options.resolver;
6389
- const buildPath = config.buildPath ?? this.options.buildPath ?? "";
6390
- if (!resolver) {
6804
+ for (const output of config.outputs) {
6805
+ const outputErrors = validator.validateOutputConfig(output);
6806
+ if (outputErrors.length > 0) {
6391
6807
  throw new ConfigurationError(
6392
- "resolver must be provided either in constructor options or build config"
6808
+ `Invalid output '${output.name}': ${validator.getErrorMessage(outputErrors)}`
6393
6809
  );
6394
6810
  }
6395
- return { resolver, buildPath };
6396
6811
  }
6397
- /**
6398
- * Resolves tokens for a specific permutation without generating output files
6399
- *
6400
- * Useful for programmatic access to resolved token values, testing,
6401
- * or implementing custom output logic. Returns fully resolved tokens
6402
- * with all references and aliases resolved.
6403
- *
6404
- * @param resolver - Resolver configuration (file path or inline object)
6405
- * @param modifierInputs - Modifier values for this permutation (e.g., `{ theme: 'dark' }`)
6406
- * @returns Object mapping token names to resolved token objects
6407
- * @throws {FileOperationError} If resolver file cannot be read
6408
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6409
- *
6410
- * @example
6411
- * ```typescript
6412
- * const tokens = await dispersa.resolveTokens(
6413
- * './tokens.resolver.json',
6414
- * { theme: 'dark' }
6415
- * )
6416
- *
6417
- * console.log(tokens['color.background'].$value) // '#1a1a1a'
6418
- * ```
6419
- */
6420
- async resolveTokens(resolver, modifierInputs = {}) {
6421
- const { tokens } = await this.pipeline.resolve(resolver, modifierInputs);
6422
- return stripInternalTokenMetadata(tokens);
6423
- }
6424
- /**
6425
- * Resolves tokens for all permutations defined in the resolver
6426
- *
6427
- * Auto-discovers all possible permutations from the resolver's modifier
6428
- * definitions and resolves tokens for each one. Useful for generating
6429
- * comprehensive token sets or validating all theme variations.
6430
- *
6431
- * @param resolver - Resolver configuration (file path or inline object)
6432
- * @returns Array of resolved token sets with their modifier inputs
6433
- * @throws {FileOperationError} If resolver file cannot be read
6434
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6435
- *
6436
- * @example
6437
- * ```typescript
6438
- * const permutations = await dispersa.resolveAllPermutations(
6439
- * './tokens.resolver.json'
6440
- * )
6441
- *
6442
- * permutations.forEach(({ tokens, modifierInputs }) => {
6443
- * console.log(`Theme: ${modifierInputs.theme}`)
6444
- * console.log(`Tokens: ${Object.keys(tokens).length}`)
6445
- * })
6446
- * ```
6447
- */
6448
- async resolveAllPermutations(resolver) {
6449
- const permutations = await this.pipeline.resolveAllPermutations(resolver);
6450
- return permutations.map(({ tokens, modifierInputs }) => ({
6451
- tokens: stripInternalTokenMetadata(tokens),
6452
- modifierInputs
6453
- }));
6812
+ }
6813
+ async function resolvePipeline(pipeline, resolver, modifierInputs) {
6814
+ const { tokens } = await pipeline.resolve(resolver, modifierInputs);
6815
+ return stripInternalTokenMetadata(tokens);
6816
+ }
6817
+ async function build(config) {
6818
+ try {
6819
+ return await buildOrThrow(config);
6820
+ } catch (error) {
6821
+ return {
6822
+ success: false,
6823
+ outputs: [],
6824
+ errors: [toBuildError(error)]
6825
+ };
6454
6826
  }
6455
- /**
6456
- * Generates TypeScript type definitions from resolved tokens
6457
- *
6458
- * Creates a `.d.ts` file with type-safe token definitions including:
6459
- * - Token name union type for autocomplete
6460
- * - Token value types
6461
- * - Nested structure types matching token organization
6462
- *
6463
- * @param tokens - Resolved tokens object
6464
- * @param fileName - Path for the generated `.d.ts` file
6465
- * @param options - Generation options
6466
- * @param options.moduleName - Name for the exported types (default: 'Tokens')
6467
- * @returns Promise that resolves when file is written
6468
- * @throws {FileOperationError} If file cannot be written
6469
- *
6470
- * @example
6471
- * ```typescript
6472
- * const tokens = await dispersa.resolveTokens('./tokens.resolver.json')
6473
- * await dispersa.generateTypes(tokens, './src/tokens.d.ts', {
6474
- * moduleName: 'DesignTokens'
6475
- * })
6476
- *
6477
- * // Generated types can be used like:
6478
- * // const tokenName: TokenName = 'color.background'
6479
- * // const tokens: DesignTokens = { ... }
6480
- * ```
6481
- */
6482
- async generateTypes(tokens, fileName, options) {
6483
- const typeWriter = new TypeWriter();
6484
- await typeWriter.write(tokens, {
6485
- fileName,
6486
- moduleName: options?.moduleName ?? "Tokens",
6487
- includeValues: true
6488
- });
6827
+ }
6828
+ async function buildOrThrow(config) {
6829
+ const validator = createValidator();
6830
+ validateBuildConfig(validator, config);
6831
+ const { resolver, buildPath } = resolveConfig(config);
6832
+ const pipeline = createPipeline({ validation: config.validation });
6833
+ const outputProcessor = createOutputProcessor();
6834
+ const orchestrator = createOrchestrator(pipeline, outputProcessor);
6835
+ return orchestrator.build(resolver, buildPath, config);
6836
+ }
6837
+ async function buildPermutation(config, modifierInputs = {}) {
6838
+ return build({
6839
+ ...config,
6840
+ permutations: [modifierInputs]
6841
+ });
6842
+ }
6843
+ async function resolveTokens(resolver, modifierInputs = {}, validation) {
6844
+ const pipeline = createPipeline({ validation });
6845
+ return resolvePipeline(pipeline, resolver, modifierInputs);
6846
+ }
6847
+ async function lint(options) {
6848
+ const { resolver, modifierInputs = {}, validation, ...lintConfig } = options;
6849
+ const pipeline = createPipeline({ validation });
6850
+ const tokens = await resolvePipeline(pipeline, resolver, modifierInputs);
6851
+ const runner = new LintRunner({
6852
+ ...lintConfig,
6853
+ failOnError: lintConfig.failOnError ?? true
6854
+ });
6855
+ const result = await runner.run(tokens);
6856
+ if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6857
+ throw new LintError(result.issues);
6489
6858
  }
6490
- };
6859
+ return result;
6860
+ }
6861
+ async function resolveAllPermutations(resolver) {
6862
+ const pipeline = createPipeline();
6863
+ const permutations = await pipeline.resolveAllPermutations(resolver);
6864
+ return permutations.map(({ tokens, modifierInputs }) => ({
6865
+ tokens: stripInternalTokenMetadata(tokens),
6866
+ modifierInputs
6867
+ }));
6868
+ }
6869
+ async function generateTypes(tokens, fileName, options) {
6870
+ const typeWriter = new TypeWriter();
6871
+ await typeWriter.write(tokens, {
6872
+ fileName,
6873
+ moduleName: options?.moduleName ?? "Tokens",
6874
+ includeValues: true
6875
+ });
6876
+ }
6491
6877
 
6492
6878
  // src/tokens/types.ts
6493
6879
  function isColorToken(token) {
@@ -6514,6 +6900,17 @@ function isTransitionToken(token) {
6514
6900
  function isGradientToken(token) {
6515
6901
  return token.$type === "gradient";
6516
6902
  }
6903
+ function nameKebabCase() {
6904
+ return {
6905
+ transform: (token) => {
6906
+ const name = kebabCase(token.path.join(" "));
6907
+ return {
6908
+ ...token,
6909
+ name
6910
+ };
6911
+ }
6912
+ };
6913
+ }
6517
6914
  function isColorObject(value) {
6518
6915
  return typeof value === "object" && value !== null && "colorSpace" in value && "components" in value;
6519
6916
  }
@@ -6591,6 +6988,7 @@ function durationObjectToString(duration) {
6591
6988
  init_errors();
6592
6989
  init_token_utils();
6593
6990
  init_utils();
6991
+ init_metadata();
6594
6992
  var toSRGB = converter("rgb");
6595
6993
  var toP3 = converter("p3");
6596
6994
  var KOTLIN_KEYWORDS = /* @__PURE__ */ new Set([
@@ -6641,9 +7039,6 @@ function resolveColorFormat(format) {
6641
7039
  function escapeKotlinString(str) {
6642
7040
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\$/g, "\\$");
6643
7041
  }
6644
- function escapeKDoc(str) {
6645
- return str.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
6646
- }
6647
7042
  function formatKotlinNumber(value) {
6648
7043
  return Number.isInteger(value) ? `${value}.0` : String(value);
6649
7044
  }
@@ -6775,8 +7170,13 @@ var AndroidRenderer = class {
6775
7170
  const kotlinName = this.buildFlatKotlinName(token);
6776
7171
  const kotlinValue = this.formatKotlinValue(token, options, baseDepth + 1);
6777
7172
  const annotation = this.typeAnnotationSuffix(token);
6778
- if (token.$description) {
6779
- lines.push(`${valIndent}/** ${escapeKDoc(token.$description)} */`);
7173
+ const descriptionComment = buildTokenDescriptionComment(token, "kotlin");
7174
+ if (descriptionComment) {
7175
+ lines.push(`${valIndent}${descriptionComment}`);
7176
+ }
7177
+ const deprecation = buildKotlinDeprecationAnnotation(token);
7178
+ if (deprecation) {
7179
+ lines.push(`${valIndent}${deprecation}`);
6780
7180
  }
6781
7181
  lines.push(
6782
7182
  `${valIndent}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`
@@ -6812,8 +7212,13 @@ var AndroidRenderer = class {
6812
7212
  const kotlinName = toSafeIdentifier(key, KOTLIN_KEYWORDS, false);
6813
7213
  const kotlinValue = this.formatKotlinValue(token, options, depth);
6814
7214
  const annotation = this.typeAnnotationSuffix(token);
6815
- if (token.$description) {
6816
- lines.push(`${pad}/** ${escapeKDoc(token.$description)} */`);
7215
+ const descriptionComment = buildTokenDescriptionComment(token, "kotlin");
7216
+ if (descriptionComment) {
7217
+ lines.push(`${pad}${descriptionComment}`);
7218
+ }
7219
+ const deprecation = buildKotlinDeprecationAnnotation(token);
7220
+ if (deprecation) {
7221
+ lines.push(`${pad}${deprecation}`);
6817
7222
  }
6818
7223
  lines.push(`${pad}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`);
6819
7224
  }
@@ -7590,10 +7995,8 @@ function stableInputsKey(modifierInputs) {
7590
7995
 
7591
7996
  // src/renderers/css.ts
7592
7997
  init_utils();
7998
+ init_metadata();
7593
7999
  var CssRenderer = class _CssRenderer {
7594
- sanitizeCssCommentText(text) {
7595
- return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
7596
- }
7597
8000
  async format(context, options) {
7598
8001
  const opts = {
7599
8002
  preset: options?.preset ?? "bundle",
@@ -7673,12 +8076,13 @@ var CssRenderer = class _CssRenderer {
7673
8076
  }
7674
8077
  pushTokenLines(lines, token, tokens, referenceTokens, preserveReferences, indent, newline, space) {
7675
8078
  const entries = this.buildCssEntries(token, tokens, referenceTokens, preserveReferences);
7676
- if (token.$deprecated != null && token.$deprecated !== false) {
7677
- const deprecationMsg = formatDeprecationMessage(token, "", "comment");
7678
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(deprecationMsg)} */${newline}`);
8079
+ const deprecationComment = buildTokenDeprecationComment(token, "css");
8080
+ if (deprecationComment) {
8081
+ lines.push(`${indent}${deprecationComment}${newline}`);
7679
8082
  }
7680
- if (token.$description && token.$description !== "") {
7681
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(token.$description)} */${newline}`);
8083
+ const descriptionComment = buildTokenDescriptionComment(token, "css");
8084
+ if (descriptionComment) {
8085
+ lines.push(`${indent}${descriptionComment}${newline}`);
7682
8086
  }
7683
8087
  for (const entry of entries) {
7684
8088
  lines.push(`${indent}--${entry.name}:${space}${entry.value};${newline}`);
@@ -8318,6 +8722,7 @@ function cssRenderer() {
8318
8722
 
8319
8723
  // src/renderers/ios.ts
8320
8724
  init_utils();
8725
+ init_metadata();
8321
8726
  var toSRGB2 = converter("rgb");
8322
8727
  var toP32 = converter("p3");
8323
8728
  var SWIFT_TYPE_GROUP_MAP = {
@@ -8461,9 +8866,13 @@ var IosRenderer = class {
8461
8866
  const swiftValue = this.formatSwiftValue(token, options);
8462
8867
  const typeAnnotation = this.getTypeAnnotation(token);
8463
8868
  const annotation = typeAnnotation ? `: ${typeAnnotation}` : "";
8464
- const docComment = this.buildDocComment(token, indent);
8869
+ const docComment = buildTokenDescriptionComment(token, "swift");
8465
8870
  if (docComment) {
8466
- lines.push(docComment);
8871
+ lines.push(`${indent}${docComment}`);
8872
+ }
8873
+ const deprecationAttr = buildSwiftDeprecationAttribute(token);
8874
+ if (deprecationAttr) {
8875
+ lines.push(`${indent}${deprecationAttr}`);
8467
8876
  }
8468
8877
  lines.push(`${indent}${access3} ${staticPrefix}${swiftName}${annotation} = ${swiftValue}`);
8469
8878
  }
@@ -8478,15 +8887,6 @@ var IosRenderer = class {
8478
8887
  }
8479
8888
  return Array.from(imports).sort();
8480
8889
  }
8481
- /**
8482
- * Builds a `///` doc comment from a token's `$description`, if present.
8483
- */
8484
- buildDocComment(token, indent) {
8485
- if (!token.$description) {
8486
- return void 0;
8487
- }
8488
- return `${indent}/// ${token.$description}`;
8489
- }
8490
8890
  /**
8491
8891
  * Builds a qualified Swift name from a token's path, preserving parent
8492
8892
  * hierarchy segments to avoid duplicate identifiers.
@@ -8877,6 +9277,7 @@ function iosRenderer() {
8877
9277
  // src/renderers/js-module.ts
8878
9278
  init_utils();
8879
9279
  init_token_utils();
9280
+ init_metadata();
8880
9281
  var JsModuleRenderer = class {
8881
9282
  async format(context, options) {
8882
9283
  const opts = {
@@ -8914,17 +9315,10 @@ var JsModuleRenderer = class {
8914
9315
  return outputTree(files);
8915
9316
  }
8916
9317
  async formatTokens(tokens, options) {
8917
- const opts = {
8918
- preset: options.preset ?? "standalone",
8919
- structure: options.structure ?? "nested",
8920
- minify: options.minify ?? false,
8921
- moduleName: options.moduleName ?? "tokens",
8922
- generateHelper: options.generateHelper ?? false
8923
- };
8924
9318
  const lines = [];
8925
- lines.push(...this.formatAsObject(tokens, opts));
9319
+ lines.push(...this.formatAsObject(tokens, options));
8926
9320
  const code = lines.join("\n");
8927
- if (opts.minify) {
9321
+ if (options.minify) {
8928
9322
  return code;
8929
9323
  }
8930
9324
  return await prettier.format(code, {
@@ -8937,20 +9331,53 @@ var JsModuleRenderer = class {
8937
9331
  trailingComma: "es5"
8938
9332
  });
8939
9333
  }
8940
- /**
8941
- * Format as default export object
8942
- */
8943
9334
  formatAsObject(tokens, options) {
8944
9335
  const lines = [];
8945
- const tokenObj = this.tokensToPlainObject(tokens, options.structure);
9336
+ const tokenMap = this.buildTokenMap(tokens);
8946
9337
  const varName = options.moduleName !== "" ? options.moduleName : "tokens";
8947
- lines.push(`const ${varName} = {`);
8948
- this.addObjectProperties(lines, tokenObj, 1);
8949
- lines.push("}");
9338
+ if (options.structure === "flat") {
9339
+ lines.push(`const ${varName} = {`);
9340
+ this.addFlatProperties(lines, tokens, 1);
9341
+ lines.push("}");
9342
+ } else {
9343
+ lines.push(`const ${varName} = {`);
9344
+ const tokenObj = this.tokensToPlainObject(tokens, "nested");
9345
+ this.addNestedProperties(lines, tokenObj, tokenMap, 1);
9346
+ lines.push("}");
9347
+ }
8950
9348
  lines.push("");
8951
9349
  lines.push(`export default ${varName}`);
8952
9350
  return lines;
8953
9351
  }
9352
+ buildTokenMap(tokens) {
9353
+ const map = /* @__PURE__ */ new Map();
9354
+ for (const [name, token] of Object.entries(tokens)) {
9355
+ map.set(name, token);
9356
+ }
9357
+ return map;
9358
+ }
9359
+ addFlatProperties(lines, tokens, indent) {
9360
+ const indentStr2 = " ".repeat(indent);
9361
+ const sortedEntries = getSortedTokenEntries(tokens);
9362
+ for (let i = 0; i < sortedEntries.length; i++) {
9363
+ const [name, token] = sortedEntries[i];
9364
+ const isLast = i === sortedEntries.length - 1;
9365
+ this.pushTokenComments(lines, token, indentStr2);
9366
+ lines.push(
9367
+ `${indentStr2}${this.quoteKey(name)}: ${JSON.stringify(token.$value)}${isLast ? "" : ","}`
9368
+ );
9369
+ }
9370
+ }
9371
+ pushTokenComments(lines, token, indent) {
9372
+ const deprecationComment = buildTokenDeprecationComment(token, "js");
9373
+ if (deprecationComment) {
9374
+ lines.push(`${indent}${deprecationComment}`);
9375
+ }
9376
+ const descriptionComment = buildTokenDescriptionComment(token, "js");
9377
+ if (descriptionComment) {
9378
+ lines.push(`${indent}${descriptionComment}`);
9379
+ }
9380
+ }
8954
9381
  tokensToPlainObject(tokens, structure) {
8955
9382
  if (structure === "nested") {
8956
9383
  return buildNestedTokenObject(tokens, (token) => token.$value);
@@ -8961,7 +9388,7 @@ var JsModuleRenderer = class {
8961
9388
  }
8962
9389
  return result;
8963
9390
  }
8964
- addObjectProperties(lines, obj, indent) {
9391
+ addNestedProperties(lines, obj, tokenMap, indent) {
8965
9392
  const indentStr2 = " ".repeat(indent);
8966
9393
  const entries = Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
8967
9394
  for (let i = 0; i < entries.length; i++) {
@@ -8973,19 +9400,20 @@ var JsModuleRenderer = class {
8973
9400
  const isLast = i === entries.length - 1;
8974
9401
  const isNestedObject = typeof value === "object" && value !== null && !Array.isArray(value);
8975
9402
  if (!isNestedObject) {
9403
+ const token = tokenMap.get(key);
9404
+ if (token) {
9405
+ this.pushTokenComments(lines, token, indentStr2);
9406
+ }
8976
9407
  lines.push(
8977
9408
  `${indentStr2}${this.quoteKey(key)}: ${JSON.stringify(value)}${isLast ? "" : ","}`
8978
9409
  );
8979
9410
  continue;
8980
9411
  }
8981
9412
  lines.push(`${indentStr2}${this.quoteKey(key)}: {`);
8982
- this.addObjectProperties(lines, value, indent + 1);
9413
+ this.addNestedProperties(lines, value, tokenMap, indent + 1);
8983
9414
  lines.push(`${indentStr2}}${isLast ? "" : ","}`);
8984
9415
  }
8985
9416
  }
8986
- /**
8987
- * Quote key if necessary
8988
- */
8989
9417
  quoteKey(key) {
8990
9418
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
8991
9419
  return key;
@@ -9221,6 +9649,7 @@ function resolveOptions(options) {
9221
9649
 
9222
9650
  // src/renderers/tailwind.ts
9223
9651
  init_utils();
9652
+ init_metadata();
9224
9653
  var TAILWIND_NAMESPACE_MAP = {
9225
9654
  color: "color",
9226
9655
  dimension: "spacing",
@@ -9274,6 +9703,14 @@ var TailwindRenderer = class {
9274
9703
  for (const [, token] of getSortedTokenEntries(tokens)) {
9275
9704
  const varName = this.buildVariableName(token);
9276
9705
  const varValue = this.formatValue(token);
9706
+ const deprecationComment = buildTokenDeprecationComment(token, "tailwind");
9707
+ if (deprecationComment) {
9708
+ lines.push(`${indent}${deprecationComment}${newline}`);
9709
+ }
9710
+ const descriptionComment = buildTokenDescriptionComment(token, "tailwind");
9711
+ if (descriptionComment) {
9712
+ lines.push(`${indent}${descriptionComment}${newline}`);
9713
+ }
9277
9714
  lines.push(`${indent}--${varName}:${space}${varValue};${newline}`);
9278
9715
  }
9279
9716
  lines.push(`}${newline}`);
@@ -9300,6 +9737,14 @@ var TailwindRenderer = class {
9300
9737
  for (const [, token] of getSortedTokenEntries(tokens)) {
9301
9738
  const varName = this.buildVariableName(token);
9302
9739
  const varValue = this.formatValue(token);
9740
+ const deprecationComment = buildTokenDeprecationComment(token, "tailwind");
9741
+ if (deprecationComment) {
9742
+ lines.push(`${tokenIndent}${deprecationComment}${newline}`);
9743
+ }
9744
+ const descriptionComment = buildTokenDescriptionComment(token, "tailwind");
9745
+ if (descriptionComment) {
9746
+ lines.push(`${tokenIndent}${descriptionComment}${newline}`);
9747
+ }
9303
9748
  lines.push(`${tokenIndent}--${varName}:${space}${varValue};${newline}`);
9304
9749
  }
9305
9750
  if (hasMediaQuery) {
@@ -9460,7 +9905,7 @@ function css(config) {
9460
9905
  file,
9461
9906
  renderer: cssRenderer(),
9462
9907
  options: { preset, ...rendererOptions },
9463
- transforms,
9908
+ transforms: [nameKebabCase(), ...transforms ?? []],
9464
9909
  filters,
9465
9910
  hooks
9466
9911
  };
@@ -9572,12 +10017,19 @@ init_errors();
9572
10017
  * This source code is licensed under the MIT license found in the
9573
10018
  * LICENSE file in the root directory of this source tree.
9574
10019
  */
10020
+ /**
10021
+ * @license MIT
10022
+ * Copyright (c) 2025-present Dispersa
10023
+ *
10024
+ * This source code is licensed under the MIT license found in the
10025
+ * LICENSE file in the root directory of this source tree.
10026
+ */
9575
10027
  /**
9576
10028
  * @license
9577
10029
  * Copyright (c) 2025 Dispersa Contributors
9578
10030
  * SPDX-License-Identifier: MIT
9579
10031
  */
9580
10032
 
9581
- export { BasePermutationError, CircularReferenceError, ConfigurationError, Dispersa, DispersaError, FileOperationError, ModifierError, TokenReferenceError, ValidationError, android, css, defineRenderer, ios, isBorderToken, isColorToken, isDimensionToken, isDurationToken, isGradientToken, isOutputTree, isShadowToken, isTransitionToken, isTypographyToken, js, json, outputTree, tailwind };
10033
+ export { BasePermutationError, CircularReferenceError, ConfigurationError, DispersaError, FileOperationError, LintError, ModifierError, TokenReferenceError, ValidationError, android, build, buildOrThrow, buildPermutation, css, defineRenderer, generateTypes, ios, isBorderToken, isColorToken, isDimensionToken, isDurationToken, isGradientToken, isOutputTree, isShadowToken, isTransitionToken, isTypographyToken, js, json, lint, outputTree, resolveAllPermutations, resolveTokens, tailwind };
9582
10034
  //# sourceMappingURL=index.js.map
9583
10035
  //# sourceMappingURL=index.js.map