dispersa 1.1.0 → 1.2.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.
package/dist/index.js CHANGED
@@ -164,8 +164,39 @@ function getPureAliasReferenceName(value) {
164
164
  const match = /^\{([^}]+)\}$/.exec(value);
165
165
  return match?.[1]?.trim();
166
166
  }
167
+ function rewriteRootReferences(value) {
168
+ if (typeof value === "string") {
169
+ return ROOT_REF_PATTERN.test(value) ? value.replace(ROOT_REF_PATTERN, "}") : value;
170
+ }
171
+ if (Array.isArray(value)) {
172
+ let changed = false;
173
+ const mapped = value.map((item) => {
174
+ const rewritten = rewriteRootReferences(item);
175
+ if (rewritten !== item) {
176
+ changed = true;
177
+ }
178
+ return rewritten;
179
+ });
180
+ return changed ? mapped : value;
181
+ }
182
+ if (typeof value === "object" && value !== null) {
183
+ let changed = false;
184
+ const result = {};
185
+ for (const [k, v] of Object.entries(value)) {
186
+ const rewritten = rewriteRootReferences(v);
187
+ if (rewritten !== v) {
188
+ changed = true;
189
+ }
190
+ result[k] = rewritten;
191
+ }
192
+ return changed ? result : value;
193
+ }
194
+ return value;
195
+ }
196
+ var ROOT_REF_PATTERN;
167
197
  var init_token_utils = __esm({
168
198
  "src/shared/utils/token-utils.ts"() {
199
+ ROOT_REF_PATTERN = /\.\$root\}/g;
169
200
  }
170
201
  });
171
202
 
@@ -4150,9 +4181,12 @@ var BuildOrchestrator = class {
4150
4181
  resolver,
4151
4182
  config.transforms,
4152
4183
  config.preprocessors,
4153
- config.filters,
4154
- config.lint
4184
+ config.filters
4155
4185
  );
4186
+ if (config.lint?.enabled) {
4187
+ const tokenSets = permutations2.map((p) => p.tokens);
4188
+ await this.pipeline.runLintOnPermutations(tokenSets, config.lint);
4189
+ }
4156
4190
  return this.executeBuild(buildPath, config, permutations2, resolver);
4157
4191
  }
4158
4192
  const permutations = await Promise.all(
@@ -4162,12 +4196,15 @@ var BuildOrchestrator = class {
4162
4196
  modifierInputs,
4163
4197
  config.transforms,
4164
4198
  config.preprocessors,
4165
- config.filters,
4166
- config.lint
4199
+ config.filters
4167
4200
  );
4168
4201
  return { tokens, modifierInputs: resolvedInputs };
4169
4202
  })
4170
4203
  );
4204
+ if (config.lint?.enabled) {
4205
+ const tokenSets = permutations.map((p) => p.tokens);
4206
+ await this.pipeline.runLintOnPermutations(tokenSets, config.lint);
4207
+ }
4171
4208
  return this.executeBuild(buildPath, config, permutations, resolver);
4172
4209
  }
4173
4210
  /**
@@ -4775,6 +4812,34 @@ var LintRunner = class {
4775
4812
  this.pluginLoader.clearCache();
4776
4813
  this.resolvedConfig = null;
4777
4814
  }
4815
+ /**
4816
+ * Run lint on multiple token sets with deduplication
4817
+ *
4818
+ * When running lint across multiple permutations (e.g., light/dark themes),
4819
+ * the same issue may appear multiple times. This method deduplicates issues
4820
+ * by ruleId, tokenName, and message.
4821
+ *
4822
+ * Use this for both standalone lint and build lint to ensure identical output.
4823
+ *
4824
+ * @param tokenSets - Array of resolved token sets to lint
4825
+ * @returns Combined lint result with deduplicated issues
4826
+ */
4827
+ async runMultiple(tokenSets) {
4828
+ const results = await Promise.all(tokenSets.map((tokens) => this.run(tokens)));
4829
+ const allIssues = results.flatMap((result) => result.issues);
4830
+ const seen = /* @__PURE__ */ new Set();
4831
+ const deduplicated = [];
4832
+ for (const issue of allIssues) {
4833
+ const key = `${issue.ruleId}:${issue.tokenName}:${issue.message}`;
4834
+ if (!seen.has(key)) {
4835
+ seen.add(key);
4836
+ deduplicated.push(issue);
4837
+ }
4838
+ }
4839
+ const errorCount = deduplicated.filter((i) => i.severity === "error").length;
4840
+ const warningCount = deduplicated.filter((i) => i.severity === "warn").length;
4841
+ return { issues: deduplicated, errorCount, warningCount };
4842
+ }
4778
4843
  };
4779
4844
 
4780
4845
  // src/shared/constants.ts
@@ -5923,17 +5988,35 @@ var ResolutionEngine = class {
5923
5988
  /**
5924
5989
  * Merge two token objects (later values override earlier ones)
5925
5990
  */
5926
- mergeTokens(target, source) {
5991
+ mergeTokens(target, source, path7 = []) {
5927
5992
  const result = { ...target };
5928
5993
  for (const [key, value] of Object.entries(source)) {
5929
- const isGroup = typeof value === "object" && value !== null && !Array.isArray(value) && !("$value" in value);
5930
- if (!isGroup) {
5994
+ const currentPath = [...path7, key];
5995
+ const isSourceGroup = typeof value === "object" && value !== null && !Array.isArray(value) && !("$value" in value);
5996
+ const targetValue = target[key];
5997
+ const isTargetGroup = typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && !("$value" in targetValue);
5998
+ if (targetValue !== void 0 && !key.startsWith("$")) {
5999
+ if (isSourceGroup && !isTargetGroup) {
6000
+ this.validationHandler.warn(
6001
+ `Token '${currentPath.join(".")}' is being replaced by a group. This may cause loss of existing children.`
6002
+ );
6003
+ } else if (!isSourceGroup && isTargetGroup) {
6004
+ const targetHasChildren = Object.keys(targetValue).some((k) => !k.startsWith("$"));
6005
+ if (targetHasChildren) {
6006
+ this.validationHandler.warn(
6007
+ `Group '${currentPath.join(".")}' is being replaced by a token. Existing children will be lost.`
6008
+ );
6009
+ }
6010
+ }
6011
+ }
6012
+ if (!isSourceGroup) {
5931
6013
  result[key] = value;
5932
6014
  continue;
5933
6015
  }
5934
6016
  result[key] = this.mergeTokens(
5935
6017
  result[key] ?? {},
5936
- value
6018
+ value,
6019
+ currentPath
5937
6020
  );
5938
6021
  }
5939
6022
  return result;
@@ -5973,6 +6056,7 @@ var ResolutionEngine = class {
5973
6056
 
5974
6057
  // src/build/pipeline/token-pipeline.ts
5975
6058
  init_errors();
6059
+ init_token_utils();
5976
6060
  init_validation_handler();
5977
6061
  init_errors();
5978
6062
 
@@ -6434,40 +6518,12 @@ var TokenParser = class {
6434
6518
  };
6435
6519
 
6436
6520
  // src/build/pipeline/token-pipeline.ts
6437
- var ROOT_REF_PATTERN = /\.\$root\}/g;
6438
- function rewriteRootReferences(value) {
6439
- if (typeof value === "string") {
6440
- return ROOT_REF_PATTERN.test(value) ? value.replace(ROOT_REF_PATTERN, "}") : value;
6441
- }
6442
- if (Array.isArray(value)) {
6443
- let changed = false;
6444
- const mapped = value.map((item) => {
6445
- const rewritten = rewriteRootReferences(item);
6446
- if (rewritten !== item) changed = true;
6447
- return rewritten;
6448
- });
6449
- return changed ? mapped : value;
6450
- }
6451
- if (typeof value === "object" && value !== null) {
6452
- let changed = false;
6453
- const result = {};
6454
- for (const [k, v] of Object.entries(value)) {
6455
- const rewritten = rewriteRootReferences(v);
6456
- if (rewritten !== v) changed = true;
6457
- result[k] = rewritten;
6458
- }
6459
- return changed ? result : value;
6460
- }
6461
- return value;
6462
- }
6463
6521
  var TokenPipeline = class {
6464
6522
  options;
6465
6523
  validationHandler;
6466
6524
  resolverLoader;
6467
6525
  tokenParser;
6468
6526
  aliasResolver;
6469
- lintRunner = null;
6470
- lintConfigCache = null;
6471
6527
  constructor(options = {}) {
6472
6528
  this.options = options;
6473
6529
  this.validationHandler = new ValidationHandler(options.validation);
@@ -6487,22 +6543,22 @@ var TokenPipeline = class {
6487
6543
  * 6. Parse and flatten token structure
6488
6544
  * 7. Resolve alias references
6489
6545
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
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
6546
+ * 9. Apply filters (if provided) — runs first to remove tokens before transforms
6547
+ * 10. Apply transforms (if provided) — runs on the already-filtered token set
6493
6548
  *
6494
6549
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
6495
6550
  *
6551
+ * Note: Linting is handled separately via runLintOnPermutations() to enable
6552
+ * deduplication across multiple permutations.
6553
+ *
6496
6554
  * @param resolver - Either a file path (string) or an inline ResolverDocument object
6497
6555
  * @param modifierInputs - Modifier values for this permutation
6498
6556
  * @param transformList - Optional transforms to apply
6499
6557
  * @param preprocessorList - Optional preprocessors to apply
6500
6558
  * @param filterList - Optional filters to apply before transforms
6501
- * @param lintConfig - Optional lint configuration for this run
6502
- * @returns Final tokens, resolution engine, and lint result
6559
+ * @returns Final tokens and resolution engine
6503
6560
  */
6504
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6505
- const effectiveLintConfig = lintConfig ?? this.options.lint;
6561
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6506
6562
  const stage1 = await this.loadResolver(resolver);
6507
6563
  const engine = this.createEngine(stage1);
6508
6564
  const result = await this.runPipelineStages(
@@ -6510,14 +6566,12 @@ var TokenPipeline = class {
6510
6566
  modifierInputs,
6511
6567
  preprocessorList,
6512
6568
  filterList,
6513
- transformList,
6514
- effectiveLintConfig
6569
+ transformList
6515
6570
  );
6516
6571
  return {
6517
6572
  tokens: result.tokens,
6518
6573
  resolutionEngine: result.resolutionEngine,
6519
- modifierInputs: result.modifierInputs,
6520
- lintResult: result.lintResult
6574
+ modifierInputs: result.modifierInputs
6521
6575
  };
6522
6576
  }
6523
6577
  /**
@@ -6527,15 +6581,14 @@ var TokenPipeline = class {
6527
6581
  * `resolveAllPermutations()` (parallel permutations) to keep the
6528
6582
  * stage sequence defined in exactly one place.
6529
6583
  */
6530
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6584
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6531
6585
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6532
6586
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6533
6587
  const refResolved = await this.resolveReferences(preprocessed);
6534
6588
  const flattened = this.flattenTokens(refResolved);
6535
6589
  const aliasResolved = this.resolveAliases(flattened);
6536
6590
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6537
- const linted = await this.runLintStage(rootStripped, lintConfig);
6538
- const filtered = this.applyFilterStage(linted, filterList);
6591
+ const filtered = this.applyFilterStage(rootStripped, filterList);
6539
6592
  return this.applyTransformStage(filtered, transformList);
6540
6593
  }
6541
6594
  /**
@@ -6649,45 +6702,8 @@ var TokenPipeline = class {
6649
6702
  return { ...stage, aliasResolvedTokens: result };
6650
6703
  }
6651
6704
  /**
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
6705
  /**
6690
- * Stage 10: Apply filters to the linted token set
6706
+ * Stage 9: Apply filters to tokens after alias resolution and $root stripping
6691
6707
  */
6692
6708
  applyFilterStage(stage, filterList) {
6693
6709
  let tokens = stage.aliasResolvedTokens;
@@ -6697,7 +6713,7 @@ var TokenPipeline = class {
6697
6713
  return { ...stage, aliasResolvedTokens: tokens };
6698
6714
  }
6699
6715
  /**
6700
- * Stage 11: Apply transforms to the filtered token set
6716
+ * Stage 10: Apply transforms to the filtered token set
6701
6717
  */
6702
6718
  applyTransformStage(stage, transformList) {
6703
6719
  let tokens = stage.aliasResolvedTokens;
@@ -6731,10 +6747,8 @@ var TokenPipeline = class {
6731
6747
  * @param transformList - Optional transforms to apply
6732
6748
  * @param preprocessorList - Optional preprocessors to apply
6733
6749
  * @param filterList - Optional filters to apply before transforms
6734
- * @param lintConfig - Optional lint configuration for this run
6735
6750
  */
6736
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6737
- const effectiveLintConfig = lintConfig ?? this.options.lint;
6751
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6738
6752
  const stage1 = await this.loadResolver(resolver);
6739
6753
  const discoveryEngine = this.createEngine(stage1);
6740
6754
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6747,17 +6761,39 @@ var TokenPipeline = class {
6747
6761
  modifierInputs,
6748
6762
  preprocessorList,
6749
6763
  filterList,
6750
- transformList,
6751
- effectiveLintConfig
6764
+ transformList
6752
6765
  );
6753
6766
  return {
6754
6767
  tokens: result.tokens,
6755
- modifierInputs: result.modifierInputs,
6756
- lintResult: result.lintResult
6768
+ modifierInputs: result.modifierInputs
6757
6769
  };
6758
6770
  })
6759
6771
  );
6760
6772
  }
6773
+ /**
6774
+ * Run lint on tokens from multiple permutations with deduplication
6775
+ *
6776
+ * This runs lint once on all permutation token sets combined and deduplicates
6777
+ * issues. Use this after resolveAllPermutations() for consistent lint behavior.
6778
+ */
6779
+ async runLintOnPermutations(permutationTokens, lintConfig) {
6780
+ if (!lintConfig.enabled) {
6781
+ return { issues: [], errorCount: 0, warningCount: 0 };
6782
+ }
6783
+ const runner = new LintRunner({
6784
+ plugins: lintConfig.plugins,
6785
+ rules: lintConfig.rules,
6786
+ onWarn: (msg) => this.validationHandler.warn(msg)
6787
+ });
6788
+ const result = await runner.runMultiple(permutationTokens);
6789
+ for (const issue of result.issues.filter((i) => i.severity === "warn")) {
6790
+ this.validationHandler.warn(`[${issue.ruleId}] ${issue.message} (token: ${issue.tokenName})`);
6791
+ }
6792
+ if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6793
+ throw new LintError(result.issues);
6794
+ }
6795
+ return result;
6796
+ }
6761
6797
  /**
6762
6798
  * Apply preprocessors to raw tokens
6763
6799
  */
@@ -6847,12 +6883,19 @@ async function resolveTokens(resolver, modifierInputs = {}, validation) {
6847
6883
  async function lint(options) {
6848
6884
  const { resolver, modifierInputs = {}, validation, ...lintConfig } = options;
6849
6885
  const pipeline = createPipeline({ validation });
6850
- const tokens = await resolvePipeline(pipeline, resolver, modifierInputs);
6851
6886
  const runner = new LintRunner({
6852
6887
  ...lintConfig,
6853
6888
  failOnError: lintConfig.failOnError ?? true
6854
6889
  });
6855
- const result = await runner.run(tokens);
6890
+ let tokenSets;
6891
+ if (Object.keys(modifierInputs).length > 0) {
6892
+ const { tokens } = await pipeline.resolve(resolver, modifierInputs);
6893
+ tokenSets = [tokens];
6894
+ } else {
6895
+ const permutations = await pipeline.resolveAllPermutations(resolver);
6896
+ tokenSets = permutations.map((p) => p.tokens);
6897
+ }
6898
+ const result = await runner.runMultiple(tokenSets);
6856
6899
  if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6857
6900
  throw new LintError(result.issues);
6858
6901
  }