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.js CHANGED
@@ -3,6 +3,10 @@ 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';
7
11
  import { kebabCase } from 'change-case';
8
12
  import { converter, formatHex8, formatHex } from 'culori';
@@ -19,7 +23,7 @@ var __export = (target, all) => {
19
23
  };
20
24
 
21
25
  // src/shared/errors/index.ts
22
- var DispersaError, TokenReferenceError, CircularReferenceError, ValidationError, FileOperationError, ConfigurationError, BasePermutationError, ModifierError;
26
+ var DispersaError, TokenReferenceError, CircularReferenceError, ValidationError, FileOperationError, ConfigurationError, BasePermutationError, ModifierError, LintError;
23
27
  var init_errors = __esm({
24
28
  "src/shared/errors/index.ts"() {
25
29
  DispersaError = class extends Error {
@@ -102,23 +106,19 @@ var init_errors = __esm({
102
106
  this.name = "ModifierError";
103
107
  }
104
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
+ };
105
118
  }
106
119
  });
107
120
 
108
121
  // src/shared/utils/token-utils.ts
109
- function formatDeprecationMessage(token, description = "", format = "bracket") {
110
- if (token.$deprecated == null || token.$deprecated === false) {
111
- return description;
112
- }
113
- const deprecationMsg = typeof token.$deprecated === "string" ? token.$deprecated : "";
114
- if (format === "comment") {
115
- const msg2 = deprecationMsg ? ` ${deprecationMsg}` : "";
116
- return `DEPRECATED${msg2}`;
117
- }
118
- const msg = deprecationMsg ? `: ${deprecationMsg}` : "";
119
- const prefix = `[DEPRECATED${msg}]`;
120
- return description ? `${prefix} ${description}` : prefix;
121
- }
122
122
  function stripInternalTokenMetadata(tokens) {
123
123
  const cleaned = {};
124
124
  for (const [name, token] of Object.entries(tokens)) {
@@ -2472,7 +2472,7 @@ var init_schemas = __esm({
2472
2472
  });
2473
2473
 
2474
2474
  // src/validation/config-schemas.ts
2475
- var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2475
+ var resolverSchemaRef, basePluginProperties, commonRendererOptionsProperties, transformPluginSchema, rendererPluginSchema, filterPluginSchema, preprocessorPluginSchema, lintConfigSchema, outputConfigSchema, dispersaOptionsSchema, buildConfigSchema;
2476
2476
  var init_config_schemas = __esm({
2477
2477
  "src/validation/config-schemas.ts"() {
2478
2478
  init_schemas();
@@ -2623,6 +2623,42 @@ var init_config_schemas = __esm({
2623
2623
  }
2624
2624
  }
2625
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
+ };
2626
2662
  outputConfigSchema = {
2627
2663
  $schema: "http://json-schema.org/draft-07/schema#",
2628
2664
  type: "object",
@@ -2740,10 +2776,20 @@ var init_config_schemas = __esm({
2740
2776
  description: "Resolver configuration - file path or ResolverDocument object"
2741
2777
  },
2742
2778
  buildPath: { type: "string" },
2779
+ validation: {
2780
+ type: "object",
2781
+ properties: {
2782
+ mode: { type: "string", enum: ["error", "warn", "off"] }
2783
+ }
2784
+ },
2743
2785
  hooks: {
2744
2786
  type: "object",
2745
2787
  description: "Global build lifecycle hooks (functions, validated at runtime)",
2746
2788
  additionalProperties: true
2789
+ },
2790
+ lint: {
2791
+ ...lintConfigSchema,
2792
+ description: "Linting configuration"
2747
2793
  }
2748
2794
  },
2749
2795
  additionalProperties: false
@@ -3503,6 +3549,94 @@ var init_utils = __esm({
3503
3549
  }
3504
3550
  });
3505
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
+
3506
3640
  // src/renderers/bundlers/js.ts
3507
3641
  var js_exports = {};
3508
3642
  __export(js_exports, {
@@ -3607,7 +3741,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3607
3741
  }
3608
3742
  const metadata = buildMetadata(resolver);
3609
3743
  const jsBlocks = [];
3610
- for (const { tokens, modifierInputs } of bundleData) {
3744
+ for (const { tokens, modifierInputs, isBase } of bundleData) {
3611
3745
  const cleanTokens = stripInternalMetadata(tokens);
3612
3746
  const key = buildStableDashKey({
3613
3747
  modifierInputs,
@@ -3615,10 +3749,30 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3615
3749
  defaults: metadata.defaults
3616
3750
  });
3617
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
+ }
3618
3760
  const formattedJs = await formatTokens(cleanTokens);
3619
3761
  const tokenObject = extractObjectFromJsModule(formattedJs);
3620
3762
  const indentedObject = tokenObject.replace(/\n/g, "\n ");
3621
- 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}
3622
3776
  ${JSON.stringify(camelKey)}: ${indentedObject}`);
3623
3777
  }
3624
3778
  return assembleJsBundle(metadata, jsBlocks, options?.generateHelper ?? false);
@@ -3626,6 +3780,7 @@ async function bundleAsJsModule(bundleData, resolver, options, formatTokens) {
3626
3780
  var init_js = __esm({
3627
3781
  "src/renderers/bundlers/js.ts"() {
3628
3782
  init_errors();
3783
+ init_metadata();
3629
3784
  init_utils();
3630
3785
  }
3631
3786
  });
@@ -3995,7 +4150,8 @@ var BuildOrchestrator = class {
3995
4150
  resolver,
3996
4151
  config.transforms,
3997
4152
  config.preprocessors,
3998
- config.filters
4153
+ config.filters,
4154
+ config.lint
3999
4155
  );
4000
4156
  return this.executeBuild(buildPath, config, permutations2, resolver);
4001
4157
  }
@@ -4006,7 +4162,8 @@ var BuildOrchestrator = class {
4006
4162
  modifierInputs,
4007
4163
  config.transforms,
4008
4164
  config.preprocessors,
4009
- config.filters
4165
+ config.filters,
4166
+ config.lint
4010
4167
  );
4011
4168
  return { tokens, modifierInputs: resolvedInputs };
4012
4169
  })
@@ -4267,7 +4424,358 @@ var OutputProcessor = class {
4267
4424
 
4268
4425
  // src/build/pipeline/token-pipeline.ts
4269
4426
  init_resolver_loader();
4270
- 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
+ };
4271
4779
 
4272
4780
  // src/shared/constants.ts
4273
4781
  var DEFAULT_MAX_ALIAS_DEPTH = 10;
@@ -5462,6 +5970,10 @@ var ResolutionEngine = class {
5462
5970
  return typeof obj === "object" && obj !== null && "contexts" in obj && typeof obj.contexts === "object";
5463
5971
  }
5464
5972
  };
5973
+
5974
+ // src/build/pipeline/token-pipeline.ts
5975
+ init_errors();
5976
+ init_validation_handler();
5465
5977
  init_errors();
5466
5978
 
5467
5979
  // src/shared/utils/path-utils.ts
@@ -5954,6 +6466,8 @@ var TokenPipeline = class {
5954
6466
  resolverLoader;
5955
6467
  tokenParser;
5956
6468
  aliasResolver;
6469
+ lintRunner = null;
6470
+ lintConfigCache = null;
5957
6471
  constructor(options = {}) {
5958
6472
  this.options = options;
5959
6473
  this.validationHandler = new ValidationHandler(options.validation);
@@ -5973,8 +6487,9 @@ var TokenPipeline = class {
5973
6487
  * 6. Parse and flatten token structure
5974
6488
  * 7. Resolve alias references
5975
6489
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
5976
- * 9. Apply filters (if provided) — runs first to remove tokens before transforms
5977
- * 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
5978
6493
  *
5979
6494
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
5980
6495
  *
@@ -5983,9 +6498,11 @@ var TokenPipeline = class {
5983
6498
  * @param transformList - Optional transforms to apply
5984
6499
  * @param preprocessorList - Optional preprocessors to apply
5985
6500
  * @param filterList - Optional filters to apply before transforms
5986
- * @returns Final tokens and resolution engine
6501
+ * @param lintConfig - Optional lint configuration for this run
6502
+ * @returns Final tokens, resolution engine, and lint result
5987
6503
  */
5988
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6504
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6505
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
5989
6506
  const stage1 = await this.loadResolver(resolver);
5990
6507
  const engine = this.createEngine(stage1);
5991
6508
  const result = await this.runPipelineStages(
@@ -5993,29 +6510,32 @@ var TokenPipeline = class {
5993
6510
  modifierInputs,
5994
6511
  preprocessorList,
5995
6512
  filterList,
5996
- transformList
6513
+ transformList,
6514
+ effectiveLintConfig
5997
6515
  );
5998
6516
  return {
5999
6517
  tokens: result.tokens,
6000
6518
  resolutionEngine: result.resolutionEngine,
6001
- modifierInputs: result.modifierInputs
6519
+ modifierInputs: result.modifierInputs,
6520
+ lintResult: result.lintResult
6002
6521
  };
6003
6522
  }
6004
6523
  /**
6005
- * Run pipeline stages 3-9 on a pre-created engine
6524
+ * Run pipeline stages 3-11 on a pre-created engine
6006
6525
  *
6007
6526
  * Shared by both `resolve()` (single permutation) and
6008
6527
  * `resolveAllPermutations()` (parallel permutations) to keep the
6009
6528
  * stage sequence defined in exactly one place.
6010
6529
  */
6011
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6530
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6012
6531
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6013
6532
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6014
6533
  const refResolved = await this.resolveReferences(preprocessed);
6015
6534
  const flattened = this.flattenTokens(refResolved);
6016
6535
  const aliasResolved = this.resolveAliases(flattened);
6017
6536
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6018
- const filtered = this.applyFilterStage(rootStripped, filterList);
6537
+ const linted = await this.runLintStage(rootStripped, lintConfig);
6538
+ const filtered = this.applyFilterStage(linted, filterList);
6019
6539
  return this.applyTransformStage(filtered, transformList);
6020
6540
  }
6021
6541
  /**
@@ -6128,6 +6648,47 @@ var TokenPipeline = class {
6128
6648
  }
6129
6649
  return { ...stage, aliasResolvedTokens: result };
6130
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
+ */
6131
6692
  applyFilterStage(stage, filterList) {
6132
6693
  let tokens = stage.aliasResolvedTokens;
6133
6694
  if (filterList !== void 0 && filterList.length > 0) {
@@ -6136,7 +6697,7 @@ var TokenPipeline = class {
6136
6697
  return { ...stage, aliasResolvedTokens: tokens };
6137
6698
  }
6138
6699
  /**
6139
- * Stage 9: Apply transforms to the filtered token set
6700
+ * Stage 11: Apply transforms to the filtered token set
6140
6701
  */
6141
6702
  applyTransformStage(stage, transformList) {
6142
6703
  let tokens = stage.aliasResolvedTokens;
@@ -6170,8 +6731,10 @@ var TokenPipeline = class {
6170
6731
  * @param transformList - Optional transforms to apply
6171
6732
  * @param preprocessorList - Optional preprocessors to apply
6172
6733
  * @param filterList - Optional filters to apply before transforms
6734
+ * @param lintConfig - Optional lint configuration for this run
6173
6735
  */
6174
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6736
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6737
+ const effectiveLintConfig = lintConfig ?? this.options.lint;
6175
6738
  const stage1 = await this.loadResolver(resolver);
6176
6739
  const discoveryEngine = this.createEngine(stage1);
6177
6740
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6184,11 +6747,13 @@ var TokenPipeline = class {
6184
6747
  modifierInputs,
6185
6748
  preprocessorList,
6186
6749
  filterList,
6187
- transformList
6750
+ transformList,
6751
+ effectiveLintConfig
6188
6752
  );
6189
6753
  return {
6190
6754
  tokens: result.tokens,
6191
- modifierInputs: result.modifierInputs
6755
+ modifierInputs: result.modifierInputs,
6756
+ lintResult: result.lintResult
6192
6757
  };
6193
6758
  })
6194
6759
  );
@@ -6209,286 +6774,106 @@ var TokenPipeline = class {
6209
6774
  init_errors();
6210
6775
  init_token_utils();
6211
6776
  init_validator();
6212
- var Dispersa = class {
6213
- validator;
6214
- pipeline;
6215
- outputProcessor;
6216
- orchestrator;
6217
- options;
6218
- /**
6219
- * Creates a new Dispersa instance
6220
- *
6221
- * @param options - Configuration options (optional, can be empty object)
6222
- * @param options.resolver - Default resolver (file path or inline object, optional if provided at build time)
6223
- * @param options.buildPath - Default output directory for generated files (omit for in-memory mode)
6224
- * @throws {ConfigurationError} If options are invalid and validation is enabled
6225
- */
6226
- constructor(options = {}) {
6227
- this.options = options;
6228
- this.validator = new SchemaValidator();
6229
- const errors = this.validator.validateDispersaOptions(options);
6230
- if (errors.length > 0) {
6231
- throw new ConfigurationError(
6232
- `Invalid Dispersa options: ${this.validator.getErrorMessage(errors)}`
6233
- );
6234
- }
6235
- this.pipeline = new TokenPipeline({ validation: options.validation });
6236
- this.outputProcessor = new OutputProcessor();
6237
- this.orchestrator = new BuildOrchestrator(this.pipeline, this.outputProcessor);
6238
- }
6239
- /**
6240
- * Builds design tokens from a resolver configuration
6241
- *
6242
- * Processes tokens through the resolution pipeline, applies preprocessors,
6243
- * transforms, and filters, then generates output files in multiple formats
6244
- * for specified outputs.
6245
- *
6246
- * **Runtime Validation:**
6247
- * This method validates the build configuration
6248
- * and all output configurations before processing, throwing helpful errors for
6249
- * invalid inputs.
6250
- *
6251
- * **Permutation Handling:**
6252
- * - If `config.permutations` is provided and non-empty, builds those specific permutations
6253
- * - If `config.permutations` is undefined or empty, auto-discovers all permutations from resolver
6254
- *
6255
- * @param config - Build configuration
6256
- * @param config.resolver - Resolver configuration (file path or inline object, optional if set in constructor)
6257
- * @param config.outputs - Array of output configurations (renderers, transforms, filters)
6258
- * @param config.buildPath - Output directory for generated files (omit for in-memory mode, optional if set in constructor)
6259
- * @param config.transforms - Global transforms to apply to all tokens
6260
- * @param config.preprocessors - Global preprocessors to apply before parsing
6261
- * @param config.permutations - Array of modifier inputs for generating variations
6262
- * @returns Build result with success status and generated output files
6263
- * @throws {ConfigurationError} If configuration is invalid
6264
- * @throws {FileOperationError} If file operations fail
6265
- *
6266
- * @example Basic build
6267
- * ```typescript
6268
- * const dispersa = new Dispersa({
6269
- * resolver: './tokens.resolver.json',
6270
- * buildPath: './output'
6271
- * })
6272
- *
6273
- * const result = await dispersa.build({
6274
- * outputs: [
6275
- * css({
6276
- * name: 'css',
6277
- * file: 'tokens.css',
6278
- * preset: 'bundle',
6279
- * selector: ':root',
6280
- * transforms: [nameKebabCase()]
6281
- * })
6282
- * ]
6283
- * })
6284
- * ```
6285
- *
6286
- * @example With filters and multiple presets
6287
- * ```typescript
6288
- * const result = await dispersa.build({
6289
- * resolver: './tokens.resolver.json',
6290
- * outputs: [
6291
- * css({
6292
- * name: 'css',
6293
- * file: 'tokens.css',
6294
- * preset: 'bundle',
6295
- * selector: ':root', // All themes in one file
6296
- * transforms: [nameKebabCase()]
6297
- * }),
6298
- * json({
6299
- * name: 'json',
6300
- * file: 'tokens-{theme}.json', // Separate file per theme
6301
- * preset: 'standalone',
6302
- * structure: 'flat'
6303
- * })
6304
- * ],
6305
- * buildPath: './output',
6306
- * permutations: [
6307
- * { theme: 'light' },
6308
- * { theme: 'dark' }
6309
- * ]
6310
- * })
6311
- * ```
6312
- */
6313
- async build(config) {
6314
- try {
6315
- return await this.buildOrThrow(config);
6316
- } catch (error) {
6317
- return {
6318
- success: false,
6319
- outputs: [],
6320
- errors: [toBuildError(error)]
6321
- };
6322
- }
6323
- }
6324
- /**
6325
- * Builds design tokens and throws on any failure.
6326
- *
6327
- * Unlike {@link build}, which catches errors and returns them inside
6328
- * `BuildResult.errors`, this method propagates the first error as an
6329
- * exception. Use it when you want fail-fast behavior in CLI or CI workflows.
6330
- *
6331
- * @param config - Build configuration specifying resolver, outputs, transforms, etc.
6332
- * @returns A successful `BuildResult` (never contains errors)
6333
- * @throws {ConfigurationError} When the build config or resolver is invalid
6334
- * @throws {DispersaError} When token resolution, transforms, or rendering fails
6335
- *
6336
- * @example
6337
- * ```typescript
6338
- * try {
6339
- * const result = await dispersa.buildOrThrow({
6340
- * resolver: './tokens.resolver.json',
6341
- * outputs: [css({ name: 'css', file: 'tokens.css' })],
6342
- * buildPath: './output',
6343
- * })
6344
- * } catch (error) {
6345
- * process.exit(1)
6346
- * }
6347
- * ```
6348
- */
6349
- async buildOrThrow(config) {
6350
- const configErrors = this.validator.validateBuildConfig(config);
6351
- if (configErrors.length > 0) {
6352
- throw new ConfigurationError(
6353
- `Invalid build configuration: ${this.validator.getErrorMessage(configErrors)}`
6354
- );
6355
- }
6356
- for (const output of config.outputs) {
6357
- const outputErrors = this.validator.validateOutputConfig(output);
6358
- if (outputErrors.length > 0) {
6359
- throw new ConfigurationError(
6360
- `Invalid output '${output.name}': ${this.validator.getErrorMessage(outputErrors)}`
6361
- );
6362
- }
6363
- }
6364
- const { resolver, buildPath } = this.resolveConfig(config);
6365
- 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");
6366
6794
  }
6367
- /**
6368
- * Builds tokens for a single permutation with all configured outputs
6369
- *
6370
- * Convenience wrapper around `build()` that forces a single permutation.
6371
- * This means it has the same runtime validation and error semantics as `build()`:
6372
- * it returns a `BuildResult` object rather than throwing (use `buildOrThrow()` if you
6373
- * want fail-fast behavior).
6374
- *
6375
- * @param config - Build configuration
6376
- * @param modifierInputs - Modifier values (e.g., `{ theme: 'dark' }`)
6377
- * @returns Build result (success, outputs, optional errors)
6378
- */
6379
- async buildPermutation(config, modifierInputs = {}) {
6380
- return await this.build({
6381
- ...config,
6382
- permutations: [modifierInputs]
6383
- });
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
+ );
6384
6803
  }
6385
- /**
6386
- * Resolve configuration with constructor defaults
6387
- */
6388
- resolveConfig(config) {
6389
- const resolver = config.resolver ?? this.options.resolver;
6390
- const buildPath = config.buildPath ?? this.options.buildPath ?? "";
6391
- if (!resolver) {
6804
+ for (const output of config.outputs) {
6805
+ const outputErrors = validator.validateOutputConfig(output);
6806
+ if (outputErrors.length > 0) {
6392
6807
  throw new ConfigurationError(
6393
- "resolver must be provided either in constructor options or build config"
6808
+ `Invalid output '${output.name}': ${validator.getErrorMessage(outputErrors)}`
6394
6809
  );
6395
6810
  }
6396
- return { resolver, buildPath };
6397
6811
  }
6398
- /**
6399
- * Resolves tokens for a specific permutation without generating output files
6400
- *
6401
- * Useful for programmatic access to resolved token values, testing,
6402
- * or implementing custom output logic. Returns fully resolved tokens
6403
- * with all references and aliases resolved.
6404
- *
6405
- * @param resolver - Resolver configuration (file path or inline object)
6406
- * @param modifierInputs - Modifier values for this permutation (e.g., `{ theme: 'dark' }`)
6407
- * @returns Object mapping token names to resolved token objects
6408
- * @throws {FileOperationError} If resolver file cannot be read
6409
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6410
- *
6411
- * @example
6412
- * ```typescript
6413
- * const tokens = await dispersa.resolveTokens(
6414
- * './tokens.resolver.json',
6415
- * { theme: 'dark' }
6416
- * )
6417
- *
6418
- * console.log(tokens['color.background'].$value) // '#1a1a1a'
6419
- * ```
6420
- */
6421
- async resolveTokens(resolver, modifierInputs = {}) {
6422
- const { tokens } = await this.pipeline.resolve(resolver, modifierInputs);
6423
- return stripInternalTokenMetadata(tokens);
6424
- }
6425
- /**
6426
- * Resolves tokens for all permutations defined in the resolver
6427
- *
6428
- * Auto-discovers all possible permutations from the resolver's modifier
6429
- * definitions and resolves tokens for each one. Useful for generating
6430
- * comprehensive token sets or validating all theme variations.
6431
- *
6432
- * @param resolver - Resolver configuration (file path or inline object)
6433
- * @returns Array of resolved token sets with their modifier inputs
6434
- * @throws {FileOperationError} If resolver file cannot be read
6435
- * @throws {TokenReferenceError} If token references cannot be resolved (when validate is enabled)
6436
- *
6437
- * @example
6438
- * ```typescript
6439
- * const permutations = await dispersa.resolveAllPermutations(
6440
- * './tokens.resolver.json'
6441
- * )
6442
- *
6443
- * permutations.forEach(({ tokens, modifierInputs }) => {
6444
- * console.log(`Theme: ${modifierInputs.theme}`)
6445
- * console.log(`Tokens: ${Object.keys(tokens).length}`)
6446
- * })
6447
- * ```
6448
- */
6449
- async resolveAllPermutations(resolver) {
6450
- const permutations = await this.pipeline.resolveAllPermutations(resolver);
6451
- return permutations.map(({ tokens, modifierInputs }) => ({
6452
- tokens: stripInternalTokenMetadata(tokens),
6453
- modifierInputs
6454
- }));
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
+ };
6455
6826
  }
6456
- /**
6457
- * Generates TypeScript type definitions from resolved tokens
6458
- *
6459
- * Creates a `.d.ts` file with type-safe token definitions including:
6460
- * - Token name union type for autocomplete
6461
- * - Token value types
6462
- * - Nested structure types matching token organization
6463
- *
6464
- * @param tokens - Resolved tokens object
6465
- * @param fileName - Path for the generated `.d.ts` file
6466
- * @param options - Generation options
6467
- * @param options.moduleName - Name for the exported types (default: 'Tokens')
6468
- * @returns Promise that resolves when file is written
6469
- * @throws {FileOperationError} If file cannot be written
6470
- *
6471
- * @example
6472
- * ```typescript
6473
- * const tokens = await dispersa.resolveTokens('./tokens.resolver.json')
6474
- * await dispersa.generateTypes(tokens, './src/tokens.d.ts', {
6475
- * moduleName: 'DesignTokens'
6476
- * })
6477
- *
6478
- * // Generated types can be used like:
6479
- * // const tokenName: TokenName = 'color.background'
6480
- * // const tokens: DesignTokens = { ... }
6481
- * ```
6482
- */
6483
- async generateTypes(tokens, fileName, options) {
6484
- const typeWriter = new TypeWriter();
6485
- await typeWriter.write(tokens, {
6486
- fileName,
6487
- moduleName: options?.moduleName ?? "Tokens",
6488
- includeValues: true
6489
- });
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);
6490
6858
  }
6491
- };
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
+ }
6492
6877
 
6493
6878
  // src/tokens/types.ts
6494
6879
  function isColorToken(token) {
@@ -6603,6 +6988,7 @@ function durationObjectToString(duration) {
6603
6988
  init_errors();
6604
6989
  init_token_utils();
6605
6990
  init_utils();
6991
+ init_metadata();
6606
6992
  var toSRGB = converter("rgb");
6607
6993
  var toP3 = converter("p3");
6608
6994
  var KOTLIN_KEYWORDS = /* @__PURE__ */ new Set([
@@ -6653,9 +7039,6 @@ function resolveColorFormat(format) {
6653
7039
  function escapeKotlinString(str) {
6654
7040
  return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\$/g, "\\$");
6655
7041
  }
6656
- function escapeKDoc(str) {
6657
- return str.replace(/\*\//g, "* /").replace(/\r?\n/g, " ").trim();
6658
- }
6659
7042
  function formatKotlinNumber(value) {
6660
7043
  return Number.isInteger(value) ? `${value}.0` : String(value);
6661
7044
  }
@@ -6787,8 +7170,13 @@ var AndroidRenderer = class {
6787
7170
  const kotlinName = this.buildFlatKotlinName(token);
6788
7171
  const kotlinValue = this.formatKotlinValue(token, options, baseDepth + 1);
6789
7172
  const annotation = this.typeAnnotationSuffix(token);
6790
- if (token.$description) {
6791
- 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}`);
6792
7180
  }
6793
7181
  lines.push(
6794
7182
  `${valIndent}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`
@@ -6824,8 +7212,13 @@ var AndroidRenderer = class {
6824
7212
  const kotlinName = toSafeIdentifier(key, KOTLIN_KEYWORDS, false);
6825
7213
  const kotlinValue = this.formatKotlinValue(token, options, depth);
6826
7214
  const annotation = this.typeAnnotationSuffix(token);
6827
- if (token.$description) {
6828
- 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}`);
6829
7222
  }
6830
7223
  lines.push(`${pad}${options.visPrefix}val ${kotlinName}${annotation} = ${kotlinValue}`);
6831
7224
  }
@@ -7602,10 +7995,8 @@ function stableInputsKey(modifierInputs) {
7602
7995
 
7603
7996
  // src/renderers/css.ts
7604
7997
  init_utils();
7998
+ init_metadata();
7605
7999
  var CssRenderer = class _CssRenderer {
7606
- sanitizeCssCommentText(text) {
7607
- return text.replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ").trim();
7608
- }
7609
8000
  async format(context, options) {
7610
8001
  const opts = {
7611
8002
  preset: options?.preset ?? "bundle",
@@ -7685,12 +8076,13 @@ var CssRenderer = class _CssRenderer {
7685
8076
  }
7686
8077
  pushTokenLines(lines, token, tokens, referenceTokens, preserveReferences, indent, newline, space) {
7687
8078
  const entries = this.buildCssEntries(token, tokens, referenceTokens, preserveReferences);
7688
- if (token.$deprecated != null && token.$deprecated !== false) {
7689
- const deprecationMsg = formatDeprecationMessage(token, "", "comment");
7690
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(deprecationMsg)} */${newline}`);
8079
+ const deprecationComment = buildTokenDeprecationComment(token, "css");
8080
+ if (deprecationComment) {
8081
+ lines.push(`${indent}${deprecationComment}${newline}`);
7691
8082
  }
7692
- if (token.$description && token.$description !== "") {
7693
- lines.push(`${indent}/* ${this.sanitizeCssCommentText(token.$description)} */${newline}`);
8083
+ const descriptionComment = buildTokenDescriptionComment(token, "css");
8084
+ if (descriptionComment) {
8085
+ lines.push(`${indent}${descriptionComment}${newline}`);
7694
8086
  }
7695
8087
  for (const entry of entries) {
7696
8088
  lines.push(`${indent}--${entry.name}:${space}${entry.value};${newline}`);
@@ -8330,6 +8722,7 @@ function cssRenderer() {
8330
8722
 
8331
8723
  // src/renderers/ios.ts
8332
8724
  init_utils();
8725
+ init_metadata();
8333
8726
  var toSRGB2 = converter("rgb");
8334
8727
  var toP32 = converter("p3");
8335
8728
  var SWIFT_TYPE_GROUP_MAP = {
@@ -8473,9 +8866,13 @@ var IosRenderer = class {
8473
8866
  const swiftValue = this.formatSwiftValue(token, options);
8474
8867
  const typeAnnotation = this.getTypeAnnotation(token);
8475
8868
  const annotation = typeAnnotation ? `: ${typeAnnotation}` : "";
8476
- const docComment = this.buildDocComment(token, indent);
8869
+ const docComment = buildTokenDescriptionComment(token, "swift");
8477
8870
  if (docComment) {
8478
- lines.push(docComment);
8871
+ lines.push(`${indent}${docComment}`);
8872
+ }
8873
+ const deprecationAttr = buildSwiftDeprecationAttribute(token);
8874
+ if (deprecationAttr) {
8875
+ lines.push(`${indent}${deprecationAttr}`);
8479
8876
  }
8480
8877
  lines.push(`${indent}${access3} ${staticPrefix}${swiftName}${annotation} = ${swiftValue}`);
8481
8878
  }
@@ -8490,15 +8887,6 @@ var IosRenderer = class {
8490
8887
  }
8491
8888
  return Array.from(imports).sort();
8492
8889
  }
8493
- /**
8494
- * Builds a `///` doc comment from a token's `$description`, if present.
8495
- */
8496
- buildDocComment(token, indent) {
8497
- if (!token.$description) {
8498
- return void 0;
8499
- }
8500
- return `${indent}/// ${token.$description}`;
8501
- }
8502
8890
  /**
8503
8891
  * Builds a qualified Swift name from a token's path, preserving parent
8504
8892
  * hierarchy segments to avoid duplicate identifiers.
@@ -8889,6 +9277,7 @@ function iosRenderer() {
8889
9277
  // src/renderers/js-module.ts
8890
9278
  init_utils();
8891
9279
  init_token_utils();
9280
+ init_metadata();
8892
9281
  var JsModuleRenderer = class {
8893
9282
  async format(context, options) {
8894
9283
  const opts = {
@@ -8926,17 +9315,10 @@ var JsModuleRenderer = class {
8926
9315
  return outputTree(files);
8927
9316
  }
8928
9317
  async formatTokens(tokens, options) {
8929
- const opts = {
8930
- preset: options.preset ?? "standalone",
8931
- structure: options.structure ?? "nested",
8932
- minify: options.minify ?? false,
8933
- moduleName: options.moduleName ?? "tokens",
8934
- generateHelper: options.generateHelper ?? false
8935
- };
8936
9318
  const lines = [];
8937
- lines.push(...this.formatAsObject(tokens, opts));
9319
+ lines.push(...this.formatAsObject(tokens, options));
8938
9320
  const code = lines.join("\n");
8939
- if (opts.minify) {
9321
+ if (options.minify) {
8940
9322
  return code;
8941
9323
  }
8942
9324
  return await prettier.format(code, {
@@ -8949,20 +9331,53 @@ var JsModuleRenderer = class {
8949
9331
  trailingComma: "es5"
8950
9332
  });
8951
9333
  }
8952
- /**
8953
- * Format as default export object
8954
- */
8955
9334
  formatAsObject(tokens, options) {
8956
9335
  const lines = [];
8957
- const tokenObj = this.tokensToPlainObject(tokens, options.structure);
9336
+ const tokenMap = this.buildTokenMap(tokens);
8958
9337
  const varName = options.moduleName !== "" ? options.moduleName : "tokens";
8959
- lines.push(`const ${varName} = {`);
8960
- this.addObjectProperties(lines, tokenObj, 1);
8961
- 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
+ }
8962
9348
  lines.push("");
8963
9349
  lines.push(`export default ${varName}`);
8964
9350
  return lines;
8965
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
+ }
8966
9381
  tokensToPlainObject(tokens, structure) {
8967
9382
  if (structure === "nested") {
8968
9383
  return buildNestedTokenObject(tokens, (token) => token.$value);
@@ -8973,7 +9388,7 @@ var JsModuleRenderer = class {
8973
9388
  }
8974
9389
  return result;
8975
9390
  }
8976
- addObjectProperties(lines, obj, indent) {
9391
+ addNestedProperties(lines, obj, tokenMap, indent) {
8977
9392
  const indentStr2 = " ".repeat(indent);
8978
9393
  const entries = Object.entries(obj).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
8979
9394
  for (let i = 0; i < entries.length; i++) {
@@ -8985,19 +9400,20 @@ var JsModuleRenderer = class {
8985
9400
  const isLast = i === entries.length - 1;
8986
9401
  const isNestedObject = typeof value === "object" && value !== null && !Array.isArray(value);
8987
9402
  if (!isNestedObject) {
9403
+ const token = tokenMap.get(key);
9404
+ if (token) {
9405
+ this.pushTokenComments(lines, token, indentStr2);
9406
+ }
8988
9407
  lines.push(
8989
9408
  `${indentStr2}${this.quoteKey(key)}: ${JSON.stringify(value)}${isLast ? "" : ","}`
8990
9409
  );
8991
9410
  continue;
8992
9411
  }
8993
9412
  lines.push(`${indentStr2}${this.quoteKey(key)}: {`);
8994
- this.addObjectProperties(lines, value, indent + 1);
9413
+ this.addNestedProperties(lines, value, tokenMap, indent + 1);
8995
9414
  lines.push(`${indentStr2}}${isLast ? "" : ","}`);
8996
9415
  }
8997
9416
  }
8998
- /**
8999
- * Quote key if necessary
9000
- */
9001
9417
  quoteKey(key) {
9002
9418
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
9003
9419
  return key;
@@ -9233,6 +9649,7 @@ function resolveOptions(options) {
9233
9649
 
9234
9650
  // src/renderers/tailwind.ts
9235
9651
  init_utils();
9652
+ init_metadata();
9236
9653
  var TAILWIND_NAMESPACE_MAP = {
9237
9654
  color: "color",
9238
9655
  dimension: "spacing",
@@ -9286,6 +9703,14 @@ var TailwindRenderer = class {
9286
9703
  for (const [, token] of getSortedTokenEntries(tokens)) {
9287
9704
  const varName = this.buildVariableName(token);
9288
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
+ }
9289
9714
  lines.push(`${indent}--${varName}:${space}${varValue};${newline}`);
9290
9715
  }
9291
9716
  lines.push(`}${newline}`);
@@ -9312,6 +9737,14 @@ var TailwindRenderer = class {
9312
9737
  for (const [, token] of getSortedTokenEntries(tokens)) {
9313
9738
  const varName = this.buildVariableName(token);
9314
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
+ }
9315
9748
  lines.push(`${tokenIndent}--${varName}:${space}${varValue};${newline}`);
9316
9749
  }
9317
9750
  if (hasMediaQuery) {
@@ -9584,12 +10017,19 @@ init_errors();
9584
10017
  * This source code is licensed under the MIT license found in the
9585
10018
  * LICENSE file in the root directory of this source tree.
9586
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
+ */
9587
10027
  /**
9588
10028
  * @license
9589
10029
  * Copyright (c) 2025 Dispersa Contributors
9590
10030
  * SPDX-License-Identifier: MIT
9591
10031
  */
9592
10032
 
9593
- 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 };
9594
10034
  //# sourceMappingURL=index.js.map
9595
10035
  //# sourceMappingURL=index.js.map