dispersa 1.0.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.cjs CHANGED
@@ -191,8 +191,39 @@ function getPureAliasReferenceName(value) {
191
191
  const match = /^\{([^}]+)\}$/.exec(value);
192
192
  return match?.[1]?.trim();
193
193
  }
194
+ function rewriteRootReferences(value) {
195
+ if (typeof value === "string") {
196
+ return ROOT_REF_PATTERN.test(value) ? value.replace(ROOT_REF_PATTERN, "}") : value;
197
+ }
198
+ if (Array.isArray(value)) {
199
+ let changed = false;
200
+ const mapped = value.map((item) => {
201
+ const rewritten = rewriteRootReferences(item);
202
+ if (rewritten !== item) {
203
+ changed = true;
204
+ }
205
+ return rewritten;
206
+ });
207
+ return changed ? mapped : value;
208
+ }
209
+ if (typeof value === "object" && value !== null) {
210
+ let changed = false;
211
+ const result = {};
212
+ for (const [k, v] of Object.entries(value)) {
213
+ const rewritten = rewriteRootReferences(v);
214
+ if (rewritten !== v) {
215
+ changed = true;
216
+ }
217
+ result[k] = rewritten;
218
+ }
219
+ return changed ? result : value;
220
+ }
221
+ return value;
222
+ }
223
+ var ROOT_REF_PATTERN;
194
224
  var init_token_utils = __esm({
195
225
  "src/shared/utils/token-utils.ts"() {
226
+ ROOT_REF_PATTERN = /\.\$root\}/g;
196
227
  }
197
228
  });
198
229
 
@@ -4177,9 +4208,12 @@ var BuildOrchestrator = class {
4177
4208
  resolver,
4178
4209
  config.transforms,
4179
4210
  config.preprocessors,
4180
- config.filters,
4181
- config.lint
4211
+ config.filters
4182
4212
  );
4213
+ if (config.lint?.enabled) {
4214
+ const tokenSets = permutations2.map((p) => p.tokens);
4215
+ await this.pipeline.runLintOnPermutations(tokenSets, config.lint);
4216
+ }
4183
4217
  return this.executeBuild(buildPath, config, permutations2, resolver);
4184
4218
  }
4185
4219
  const permutations = await Promise.all(
@@ -4189,12 +4223,15 @@ var BuildOrchestrator = class {
4189
4223
  modifierInputs,
4190
4224
  config.transforms,
4191
4225
  config.preprocessors,
4192
- config.filters,
4193
- config.lint
4226
+ config.filters
4194
4227
  );
4195
4228
  return { tokens, modifierInputs: resolvedInputs };
4196
4229
  })
4197
4230
  );
4231
+ if (config.lint?.enabled) {
4232
+ const tokenSets = permutations.map((p) => p.tokens);
4233
+ await this.pipeline.runLintOnPermutations(tokenSets, config.lint);
4234
+ }
4198
4235
  return this.executeBuild(buildPath, config, permutations, resolver);
4199
4236
  }
4200
4237
  /**
@@ -4802,6 +4839,34 @@ var LintRunner = class {
4802
4839
  this.pluginLoader.clearCache();
4803
4840
  this.resolvedConfig = null;
4804
4841
  }
4842
+ /**
4843
+ * Run lint on multiple token sets with deduplication
4844
+ *
4845
+ * When running lint across multiple permutations (e.g., light/dark themes),
4846
+ * the same issue may appear multiple times. This method deduplicates issues
4847
+ * by ruleId, tokenName, and message.
4848
+ *
4849
+ * Use this for both standalone lint and build lint to ensure identical output.
4850
+ *
4851
+ * @param tokenSets - Array of resolved token sets to lint
4852
+ * @returns Combined lint result with deduplicated issues
4853
+ */
4854
+ async runMultiple(tokenSets) {
4855
+ const results = await Promise.all(tokenSets.map((tokens) => this.run(tokens)));
4856
+ const allIssues = results.flatMap((result) => result.issues);
4857
+ const seen = /* @__PURE__ */ new Set();
4858
+ const deduplicated = [];
4859
+ for (const issue of allIssues) {
4860
+ const key = `${issue.ruleId}:${issue.tokenName}:${issue.message}`;
4861
+ if (!seen.has(key)) {
4862
+ seen.add(key);
4863
+ deduplicated.push(issue);
4864
+ }
4865
+ }
4866
+ const errorCount = deduplicated.filter((i) => i.severity === "error").length;
4867
+ const warningCount = deduplicated.filter((i) => i.severity === "warn").length;
4868
+ return { issues: deduplicated, errorCount, warningCount };
4869
+ }
4805
4870
  };
4806
4871
 
4807
4872
  // src/shared/constants.ts
@@ -5950,17 +6015,35 @@ var ResolutionEngine = class {
5950
6015
  /**
5951
6016
  * Merge two token objects (later values override earlier ones)
5952
6017
  */
5953
- mergeTokens(target, source) {
6018
+ mergeTokens(target, source, path7 = []) {
5954
6019
  const result = { ...target };
5955
6020
  for (const [key, value] of Object.entries(source)) {
5956
- const isGroup = typeof value === "object" && value !== null && !Array.isArray(value) && !("$value" in value);
5957
- if (!isGroup) {
6021
+ const currentPath = [...path7, key];
6022
+ const isSourceGroup = typeof value === "object" && value !== null && !Array.isArray(value) && !("$value" in value);
6023
+ const targetValue = target[key];
6024
+ const isTargetGroup = typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue) && !("$value" in targetValue);
6025
+ if (targetValue !== void 0 && !key.startsWith("$")) {
6026
+ if (isSourceGroup && !isTargetGroup) {
6027
+ this.validationHandler.warn(
6028
+ `Token '${currentPath.join(".")}' is being replaced by a group. This may cause loss of existing children.`
6029
+ );
6030
+ } else if (!isSourceGroup && isTargetGroup) {
6031
+ const targetHasChildren = Object.keys(targetValue).some((k) => !k.startsWith("$"));
6032
+ if (targetHasChildren) {
6033
+ this.validationHandler.warn(
6034
+ `Group '${currentPath.join(".")}' is being replaced by a token. Existing children will be lost.`
6035
+ );
6036
+ }
6037
+ }
6038
+ }
6039
+ if (!isSourceGroup) {
5958
6040
  result[key] = value;
5959
6041
  continue;
5960
6042
  }
5961
6043
  result[key] = this.mergeTokens(
5962
6044
  result[key] ?? {},
5963
- value
6045
+ value,
6046
+ currentPath
5964
6047
  );
5965
6048
  }
5966
6049
  return result;
@@ -6000,6 +6083,7 @@ var ResolutionEngine = class {
6000
6083
 
6001
6084
  // src/build/pipeline/token-pipeline.ts
6002
6085
  init_errors();
6086
+ init_token_utils();
6003
6087
  init_validation_handler();
6004
6088
  init_errors();
6005
6089
 
@@ -6461,40 +6545,12 @@ var TokenParser = class {
6461
6545
  };
6462
6546
 
6463
6547
  // src/build/pipeline/token-pipeline.ts
6464
- var ROOT_REF_PATTERN = /\.\$root\}/g;
6465
- function rewriteRootReferences(value) {
6466
- if (typeof value === "string") {
6467
- return ROOT_REF_PATTERN.test(value) ? value.replace(ROOT_REF_PATTERN, "}") : value;
6468
- }
6469
- if (Array.isArray(value)) {
6470
- let changed = false;
6471
- const mapped = value.map((item) => {
6472
- const rewritten = rewriteRootReferences(item);
6473
- if (rewritten !== item) changed = true;
6474
- return rewritten;
6475
- });
6476
- return changed ? mapped : value;
6477
- }
6478
- if (typeof value === "object" && value !== null) {
6479
- let changed = false;
6480
- const result = {};
6481
- for (const [k, v] of Object.entries(value)) {
6482
- const rewritten = rewriteRootReferences(v);
6483
- if (rewritten !== v) changed = true;
6484
- result[k] = rewritten;
6485
- }
6486
- return changed ? result : value;
6487
- }
6488
- return value;
6489
- }
6490
6548
  var TokenPipeline = class {
6491
6549
  options;
6492
6550
  validationHandler;
6493
6551
  resolverLoader;
6494
6552
  tokenParser;
6495
6553
  aliasResolver;
6496
- lintRunner = null;
6497
- lintConfigCache = null;
6498
6554
  constructor(options = {}) {
6499
6555
  this.options = options;
6500
6556
  this.validationHandler = new ValidationHandler(options.validation);
@@ -6514,22 +6570,22 @@ var TokenPipeline = class {
6514
6570
  * 6. Parse and flatten token structure
6515
6571
  * 7. Resolve alias references
6516
6572
  * 8. Strip $root from token names/paths (DTCG structural mechanism, transparent in output)
6517
- * 9. Run lint rules (if enabled)
6518
- * 10. Apply filters (if provided) — runs first to remove tokens before transforms
6519
- * 11. Apply transforms (if provided) — runs on the already-filtered token set
6573
+ * 9. Apply filters (if provided) — runs first to remove tokens before transforms
6574
+ * 10. Apply transforms (if provided) — runs on the already-filtered token set
6520
6575
  *
6521
6576
  * Each stage is explicitly typed to ensure correct order and prevent temporal coupling.
6522
6577
  *
6578
+ * Note: Linting is handled separately via runLintOnPermutations() to enable
6579
+ * deduplication across multiple permutations.
6580
+ *
6523
6581
  * @param resolver - Either a file path (string) or an inline ResolverDocument object
6524
6582
  * @param modifierInputs - Modifier values for this permutation
6525
6583
  * @param transformList - Optional transforms to apply
6526
6584
  * @param preprocessorList - Optional preprocessors to apply
6527
6585
  * @param filterList - Optional filters to apply before transforms
6528
- * @param lintConfig - Optional lint configuration for this run
6529
- * @returns Final tokens, resolution engine, and lint result
6586
+ * @returns Final tokens and resolution engine
6530
6587
  */
6531
- async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList, lintConfig) {
6532
- const effectiveLintConfig = lintConfig ?? this.options.lint;
6588
+ async resolve(resolver, modifierInputs, transformList, preprocessorList, filterList) {
6533
6589
  const stage1 = await this.loadResolver(resolver);
6534
6590
  const engine = this.createEngine(stage1);
6535
6591
  const result = await this.runPipelineStages(
@@ -6537,14 +6593,12 @@ var TokenPipeline = class {
6537
6593
  modifierInputs,
6538
6594
  preprocessorList,
6539
6595
  filterList,
6540
- transformList,
6541
- effectiveLintConfig
6596
+ transformList
6542
6597
  );
6543
6598
  return {
6544
6599
  tokens: result.tokens,
6545
6600
  resolutionEngine: result.resolutionEngine,
6546
- modifierInputs: result.modifierInputs,
6547
- lintResult: result.lintResult
6601
+ modifierInputs: result.modifierInputs
6548
6602
  };
6549
6603
  }
6550
6604
  /**
@@ -6554,15 +6608,14 @@ var TokenPipeline = class {
6554
6608
  * `resolveAllPermutations()` (parallel permutations) to keep the
6555
6609
  * stage sequence defined in exactly one place.
6556
6610
  */
6557
- async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList, lintConfig) {
6611
+ async runPipelineStages(engine, modifierInputs, preprocessorList, filterList, transformList) {
6558
6612
  const rawTokens = await this.resolveTokens(engine, modifierInputs);
6559
6613
  const preprocessed = await this.preprocessTokens(rawTokens, preprocessorList);
6560
6614
  const refResolved = await this.resolveReferences(preprocessed);
6561
6615
  const flattened = this.flattenTokens(refResolved);
6562
6616
  const aliasResolved = this.resolveAliases(flattened);
6563
6617
  const rootStripped = this.stripRootTokenNames(aliasResolved);
6564
- const linted = await this.runLintStage(rootStripped, lintConfig);
6565
- const filtered = this.applyFilterStage(linted, filterList);
6618
+ const filtered = this.applyFilterStage(rootStripped, filterList);
6566
6619
  return this.applyTransformStage(filtered, transformList);
6567
6620
  }
6568
6621
  /**
@@ -6676,45 +6729,8 @@ var TokenPipeline = class {
6676
6729
  return { ...stage, aliasResolvedTokens: result };
6677
6730
  }
6678
6731
  /**
6679
- * Stage 9: Run lint rules against tokens
6680
- *
6681
- * Linting runs after alias resolution and $root stripping but before
6682
- * filters and transforms. This ensures rules see the full token set
6683
- * with resolved values.
6684
- */
6685
- async runLintStage(stage, lintConfig) {
6686
- if (!lintConfig?.enabled) {
6687
- return stage;
6688
- }
6689
- if (!this.lintRunner || !this.isLintConfigEqual(this.lintConfigCache, lintConfig)) {
6690
- this.lintRunner = new LintRunner({
6691
- plugins: lintConfig.plugins,
6692
- rules: lintConfig.rules,
6693
- onWarn: (msg) => this.validationHandler.warn(msg)
6694
- });
6695
- this.lintConfigCache = lintConfig;
6696
- }
6697
- const lintResult = await this.lintRunner.run(stage.aliasResolvedTokens);
6698
- if (lintResult.errorCount > 0 && lintConfig.failOnError !== false) {
6699
- throw new exports.LintError(lintResult.issues);
6700
- }
6701
- for (const issue of lintResult.issues.filter((i) => i.severity === "warn")) {
6702
- this.validationHandler.warn(
6703
- `[${issue.ruleId}] ${issue.message} (token: ${issue.tokenName})`
6704
- );
6705
- }
6706
- return { ...stage, lintResult };
6707
- }
6708
- isLintConfigEqual(a, b) {
6709
- if (!a || !b) return false;
6710
- if (a.enabled !== b.enabled) return false;
6711
- if (a.failOnError !== b.failOnError) return false;
6712
- if (a.plugins !== b.plugins) return false;
6713
- if (a.rules !== b.rules) return false;
6714
- return true;
6715
- }
6716
6732
  /**
6717
- * Stage 10: Apply filters to the linted token set
6733
+ * Stage 9: Apply filters to tokens after alias resolution and $root stripping
6718
6734
  */
6719
6735
  applyFilterStage(stage, filterList) {
6720
6736
  let tokens = stage.aliasResolvedTokens;
@@ -6724,7 +6740,7 @@ var TokenPipeline = class {
6724
6740
  return { ...stage, aliasResolvedTokens: tokens };
6725
6741
  }
6726
6742
  /**
6727
- * Stage 11: Apply transforms to the filtered token set
6743
+ * Stage 10: Apply transforms to the filtered token set
6728
6744
  */
6729
6745
  applyTransformStage(stage, transformList) {
6730
6746
  let tokens = stage.aliasResolvedTokens;
@@ -6758,10 +6774,8 @@ var TokenPipeline = class {
6758
6774
  * @param transformList - Optional transforms to apply
6759
6775
  * @param preprocessorList - Optional preprocessors to apply
6760
6776
  * @param filterList - Optional filters to apply before transforms
6761
- * @param lintConfig - Optional lint configuration for this run
6762
6777
  */
6763
- async resolveAllPermutations(resolver, transformList, preprocessorList, filterList, lintConfig) {
6764
- const effectiveLintConfig = lintConfig ?? this.options.lint;
6778
+ async resolveAllPermutations(resolver, transformList, preprocessorList, filterList) {
6765
6779
  const stage1 = await this.loadResolver(resolver);
6766
6780
  const discoveryEngine = this.createEngine(stage1);
6767
6781
  const permutationInputs = discoveryEngine.resolutionEngine.generatePermutations();
@@ -6774,17 +6788,39 @@ var TokenPipeline = class {
6774
6788
  modifierInputs,
6775
6789
  preprocessorList,
6776
6790
  filterList,
6777
- transformList,
6778
- effectiveLintConfig
6791
+ transformList
6779
6792
  );
6780
6793
  return {
6781
6794
  tokens: result.tokens,
6782
- modifierInputs: result.modifierInputs,
6783
- lintResult: result.lintResult
6795
+ modifierInputs: result.modifierInputs
6784
6796
  };
6785
6797
  })
6786
6798
  );
6787
6799
  }
6800
+ /**
6801
+ * Run lint on tokens from multiple permutations with deduplication
6802
+ *
6803
+ * This runs lint once on all permutation token sets combined and deduplicates
6804
+ * issues. Use this after resolveAllPermutations() for consistent lint behavior.
6805
+ */
6806
+ async runLintOnPermutations(permutationTokens, lintConfig) {
6807
+ if (!lintConfig.enabled) {
6808
+ return { issues: [], errorCount: 0, warningCount: 0 };
6809
+ }
6810
+ const runner = new LintRunner({
6811
+ plugins: lintConfig.plugins,
6812
+ rules: lintConfig.rules,
6813
+ onWarn: (msg) => this.validationHandler.warn(msg)
6814
+ });
6815
+ const result = await runner.runMultiple(permutationTokens);
6816
+ for (const issue of result.issues.filter((i) => i.severity === "warn")) {
6817
+ this.validationHandler.warn(`[${issue.ruleId}] ${issue.message} (token: ${issue.tokenName})`);
6818
+ }
6819
+ if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6820
+ throw new exports.LintError(result.issues);
6821
+ }
6822
+ return result;
6823
+ }
6788
6824
  /**
6789
6825
  * Apply preprocessors to raw tokens
6790
6826
  */
@@ -6874,12 +6910,19 @@ async function resolveTokens(resolver, modifierInputs = {}, validation) {
6874
6910
  async function lint(options) {
6875
6911
  const { resolver, modifierInputs = {}, validation, ...lintConfig } = options;
6876
6912
  const pipeline = createPipeline({ validation });
6877
- const tokens = await resolvePipeline(pipeline, resolver, modifierInputs);
6878
6913
  const runner = new LintRunner({
6879
6914
  ...lintConfig,
6880
6915
  failOnError: lintConfig.failOnError ?? true
6881
6916
  });
6882
- const result = await runner.run(tokens);
6917
+ let tokenSets;
6918
+ if (Object.keys(modifierInputs).length > 0) {
6919
+ const { tokens } = await pipeline.resolve(resolver, modifierInputs);
6920
+ tokenSets = [tokens];
6921
+ } else {
6922
+ const permutations = await pipeline.resolveAllPermutations(resolver);
6923
+ tokenSets = permutations.map((p) => p.tokens);
6924
+ }
6925
+ const result = await runner.runMultiple(tokenSets);
6883
6926
  if (result.errorCount > 0 && lintConfig.failOnError !== false) {
6884
6927
  throw new exports.LintError(result.issues);
6885
6928
  }