Skip to content

Fix: Theme - alias value#277

Merged
github-actions[bot] merged 2 commits intomainfrom
theme-bug-fixes
Nov 11, 2025
Merged

Fix: Theme - alias value#277
github-actions[bot] merged 2 commits intomainfrom
theme-bug-fixes

Conversation

@black7375
Copy link
Contributor

@black7375 black7375 commented Nov 11, 2025

Description

fix theme implement with more test codes.

Related Issue

Summary by CodeRabbit

  • Refactor

    • Updated theme API parameter types for enhanced flexibility and token composition support.
    • Improved token resolution and processing logic for better handling of composite and semantic tokens.
    • Simplified token reference handling across theme definitions.
  • Tests

    • Expanded test coverage for complex token structures and theme integrations.
    • Added comprehensive validation for token resolution and composite value handling.

Additional context

Checklist

@changeset-bot
Copy link

changeset-bot bot commented Nov 11, 2025

⚠️ No Changeset found

Latest commit: 236463b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Contributor

Triggered from #277 by @​black7375.

Checking if we can fast forward main (26546b1) to theme-bug-fixes (236463b).

Target branch (main):

commit 26546b1a08cecbbaba4a4895d09657bdc7e7a633 (HEAD -> main, origin/main)
Author: alstjr7375 <alstjr7375@daum.net>
Date:   Thu Oct 16 00:00:00 2025 +0900

    Chore: update eslint config as consist

Pull request (theme-bug-fixes):

commit 236463ba62ad15c5ebb0bda3dc89e9d1e36e4eff (pull_request/theme-bug-fixes)
Author: alstjr7375 <alstjr7375@daum.net>
Date:   Sat Oct 18 00:00:00 2025 +0900

    Fix: `theme` - composite value

It is possible to fast forward main (26546b1) to theme-bug-fixes (236463b). If you have write access to the target repository, you can add a comment with /fast-forward to fast forward main to theme-bug-fixes.

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Walkthrough

This pull request modifies the theme token system to accept ThemeTokensInput<ThemeTokens> in public APIs (globalTheme, theme, and assignTokens), and restructures the two-pass token resolution logic. Pass 1 now derives composite descriptors and nested composites with resolved getters; Pass 2 evaluates composite resolved properties and emits CSS variables. Token processing removes direct hash modifications for aliases and introduces validation utilities for variable names and resolved token formats.

Changes

Cohort / File(s) Summary
Theme token resolution and API updates
packages/css/src/theme/index.ts
Updated public API signatures to accept ThemeTokensInput<ThemeTokens> instead of direct ThemeTokens for globalTheme, theme, and assignTokens. Modified Pass 1 and Pass 2 token resolution logic to handle composite values via descriptors and CSS variable references. Removed stripHash helper usage in semantic resolution paths; adjusted alias resolution to set resolvedTokens directly without hashing modifications. Added validation utilities (VAR_HASH_REGEX, validateHashFormatForVar, validateHashFormatForResolved) and flattenAndFilterResolvedTokens helper. Expanded test coverage with validation for complex token structures, nested themes, semantic tokens with getters, fallback variables, aliases, and composite values.

Sequence Diagram(s)

sequenceDiagram
    participant Input as Token Input
    participant Pass1 as Pass 1: Descriptor<br/>& Nesting
    participant Pass2 as Pass 2: Resolution<br/>& Vars
    participant Output as Output

    Input->>Pass1: ThemeTokensInput
    Note over Pass1: For composite values:<br/>- Derive compositeDescriptors<br/>- Create nested composite<br/>- Store resolved getter<br/>- Per-property descriptors
    Pass1->>Pass2: Intermediate descriptors
    Note over Pass2: For composite resolved property:<br/>- Evaluate getter in context<br/>- Emit CSS variable via vars<br/>- Or set resolvedTokens directly
    Pass2->>Output: var(--...) references<br/>& resolved tokens
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Public API changes: Three methods updated with new ThemeTokensInput<T> type—verify type safety and backward compatibility implications
  • Two-pass token resolution logic: Pass 1 descriptor derivation and Pass 2 composite evaluation involve dense logic with multiple conditional branches—careful review of composite handling, getter evaluation, and var() emission paths needed
  • Validation utilities: New regex and helper functions (VAR_HASH_REGEX, validateHashFormatForVar, etc.) require verification for correctness across edge cases
  • Complex test scenarios: Coverage spans semantic tokens with getters, fallbackVar, raw aliases, and nested composites—ensure all permutations are logically sound

Possibly related PRs

Poem

🐰 Hop-hop through two passes we go,
Composite descriptors steal the show,
Var() references, hash validation tight,
Theme tokens aligned, CSS burning bright!

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides only a vague statement 'fix theme implement with more test codes' without explaining what is being fixed, why, or how the changes work. Required sections lack substantive content. Expand the description with clear explanations of the bug being fixed, the approach taken, and why these token processing changes were necessary. Populate the checklist section with specific items reviewers should validate.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix: Theme - alias value' is specific and relates to a real part of the changeset, but doesn't capture the main scope of changes which involves significant API modifications, token processing refactoring, and extensive test additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch theme-bug-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26546b1 and 236463b.

📒 Files selected for processing (1)
  • packages/css/src/theme/index.ts (54 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/css/src/theme/index.ts (2)
packages/css/src/utils.ts (3)
  • camelToKebab (31-33)
  • getVarName (20-29)
  • identifierName (5-11)
packages/css/src/theme/types.ts (1)
  • TokenValue (140-140)

Comment on lines +384 to +389
if (context.aliasMap.has(resolvedPathKey)) {
setByPath(resolvedTokens, resolvedPath, computedValue);
} else {
const cssVar = getCSSVarByPath(varPath, context);
vars[cssVar] = extractCSSValue(computedValue as TokenValue);
}
Copy link

@coderabbitai coderabbitai bot Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Composite alias assignment throws at runtime.

Line 385 routes the alias result through setByPath, but that helper assigns into the resolved accessor we defined in Pass 1. When a composite token does this.alias(...), that branch runs and strict-mode assignment to a getter-only accessor throws TypeError: Cannot set property resolved of #<Object> which has only a getter>. Example:

const shadow = compositeValue({
  get resolved() {
    return this.alias(this.shadow.color);
  },
  color: "#000"
});

assignTokens({ shadow });

We need to replace the accessor with a data property via Object.defineProperty instead of assigning through it.

-        if (context.aliasMap.has(resolvedPathKey)) {
-          setByPath(resolvedTokens, resolvedPath, computedValue);
-        } else {
+        if (context.aliasMap.has(resolvedPathKey)) {
+          let compositeTarget = resolvedTokens as Record<string, unknown>;
+          for (const segment of resolvedPath.slice(0, -1)) {
+            compositeTarget = compositeTarget[segment] as Record<string, unknown>;
+          }
+
+          Object.defineProperty(compositeTarget, "resolved", {
+            value: computedValue,
+            configurable: true,
+            enumerable: true
+          });
+        } else {
           const cssVar = getCSSVarByPath(varPath, context);
           vars[cssVar] = extractCSSValue(computedValue as TokenValue);
         }
🤖 Prompt for AI Agents
In packages/css/src/theme/index.ts around lines 384 to 389, the branch that
handles alias results calls setByPath which attempts to assign into the existing
resolved accessor and throws when that accessor is getter-only; instead, detect
if the target property at resolvedPath on resolvedTokens is currently an
accessor (has only a getter) and replace it with a data property using
Object.defineProperty so the computedValue is stored as a normal
writable/enumerable/configurable value. Implement this by walking to the parent
object and key for resolvedPath, check Object.getOwnPropertyDescriptor on that
key, and if descriptor && (descriptor.get || !descriptor.writable), call
Object.defineProperty(parent, key, { value: computedValue, writable: true,
enumerable: true, configurable: true }) rather than using setByPath.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Please provide test cases in the comments.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@black7375, here are comprehensive test cases for the composite alias assignment fix:

describe.concurrent("composite alias handling", () => {
  it("handles alias in composite resolved getter without throwing", () => {
    // This test verifies the bug fix: composite alias assignment should not throw
    const result = assignTokens(
      compositeValue({
        shadow: {
          get resolved() {
            // Using alias() in a composite resolved getter should not throw
            return this.alias(this.shadow.color);
          },
          color: "#00000080"
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    // Aliased composite resolved should not create a CSS variable
    expect(normalized["--shadow"]).toBeUndefined();
    // But the color property should still have a CSS variable
    expect(normalized["--shadow-color"]).toBe("#00000080");

    // The resolved property should reference the color variable
    expect(normalizeResolvedTokens(result.resolvedTokens).shadow.resolved).toBe(
      "var(--shadow-color)"
    );
  });

  it("handles alias referencing another composite in resolved getter", () => {
    const result = assignTokens(
      compositeValue({
        shadow: {
          base: {
            get resolved() {
              return `${this.shadow.base.color} ${this.shadow.base.blur}`;
            },
            color: "#00000080",
            blur: { value: 1, unit: "rem" }
          },
          themed: {
            get resolved() {
              // Alias the entire base shadow
              return this.alias(this.shadow.base.resolved);
            },
            get color() {
              return this.shadow.base.color;
            },
            get blur() {
              return this.shadow.base.blur;
            }
          }
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    // Base shadow should have CSS variable for resolved
    expect(stripHash(normalized["--shadow-base"])).toBeDefined();
    // Themed shadow resolved should not (it's aliased)
    expect(normalized["--shadow-themed"]).toBeUndefined();

    // Both should reference the base resolved variable
    const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
    expect(normalizedTokens.shadow.base.resolved).toBe("var(--shadow-base)");
    expect(normalizedTokens.shadow.themed.resolved).toBe("var(--shadow-base)");
  });

  it("handles mixed alias and non-alias properties in composite", () => {
    const result = assignTokens(
      compositeValue({
        button: {
          primary: {
            get resolved() {
              return `${this.button.primary.bg} ${this.button.primary.border}`;
            },
            bg: "#007bff",
            border: { value: 1, unit: "px" }
          },
          secondary: {
            get resolved() {
              // Non-aliased: creates CSS var
              return `${this.button.secondary.bg} ${this.button.secondary.border}`;
            },
            get bg() {
              // Aliased: references primary
              return this.alias(this.button.primary.bg);
            },
            border: { value: 1, unit: "px" }
          }
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    // Primary should have all CSS variables
    expect(normalized["--button-primary-bg"]).toBe("#007bff");
    expect(normalized["--button-primary-border"]).toBe("1px");
    expect(stripHash(normalized["--button-primary"])).toBeDefined();

    // Secondary bg should not have CSS var (aliased)
    expect(normalized["--button-secondary-bg"]).toBeUndefined();
    // But border should
    expect(normalized["--button-secondary-border"]).toBe("1px");
    // And resolved should (not aliased)
    expect(stripHash(normalized["--button-secondary"])).toBeDefined();

    const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
    expect(normalizedTokens.button.secondary.bg).toBe(
      "var(--button-primary-bg)"
    );
  });

  it("handles nested composite with alias at multiple levels", () => {
    const result = assignTokens(
      compositeValue({
        theme: {
          light: {
            shadow: {
              get resolved() {
                return `${this.theme.light.shadow.color} ${this.theme.light.shadow.blur}`;
              },
              color: "#00000020",
              blur: { value: 0.5, unit: "rem" }
            }
          },
          dark: {
            shadow: {
              get resolved() {
                // Alias the light theme's shadow blur
                return `${this.theme.dark.shadow.color} ${this.alias(
                  this.theme.light.shadow.blur
                )}`;
              },
              color: "#ffffff40",
              get blur() {
                // Also alias at property level
                return this.alias(this.theme.light.shadow.blur);
              }
            }
          }
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    // Light theme should have all vars
    expect(normalized["--theme-light-shadow-color"]).toBe("#00000020");
    expect(normalized["--theme-light-shadow-blur"]).toBe("0.5rem");

    // Dark theme color should have var
    expect(normalized["--theme-dark-shadow-color"]).toBe("#ffffff40");
    // But blur should not (aliased)
    expect(normalized["--theme-dark-shadow-blur"]).toBeUndefined();

    const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
    expect(normalizedTokens.theme.dark.shadow.blur).toBe(
      "var(--theme-light-shadow-blur)"
    );
  });

  it("handles composite with raw and alias together", () => {
    const result = assignTokens(
      compositeValue({
        elevation: {
          base: {
            get resolved() {
              return `${this.elevation.base.color} ${this.elevation.base.blur}`;
            },
            color: "#00000033",
            blur: { value: 1, unit: "rem" }
          },
          inset: {
            get resolved() {
              // Mix raw and alias
              return `inset ${this.raw(this.elevation.base.color)} ${this.alias(
                this.elevation.base.blur
              )}`;
            },
            get color() {
              return this.raw(this.elevation.base.color);
            },
            get blur() {
              return this.alias(this.elevation.base.blur);
            }
          }
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    // Inset color should have raw value
    expect(normalized["--elevation-inset-color"]).toBe("#00000033");
    // Inset blur should not exist (aliased)
    expect(normalized["--elevation-inset-blur"]).toBeUndefined();

    const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
    expect(normalizedTokens.elevation.inset.color).toBe("#00000033");
    expect(normalizedTokens.elevation.inset.blur).toBe(
      "var(--elevation-base-blur)"
    );
  });

  it("handles self-referential composite with alias", () => {
    const result = assignTokens(
      compositeValue({
        spacing: {
          base: {
            get resolved() {
              return `${this.spacing.base.top} ${this.spacing.base.right}`;
            },
            top: { value: 1, unit: "rem" },
            right: { value: 2, unit: "rem" }
          },
          symmetric: {
            get resolved() {
              // Reference own properties with alias
              return `${this.spacing.symmetric.value} ${this.alias(
                this.spacing.symmetric.value
              )}`;
            },
            value: { value: 1.5, unit: "rem" }
          }
        }
      })
    );

    validateHashFormatForVar(result.vars);
    validateHashFormatForResolved(result.resolvedTokens);

    const normalized = normalizeVars(result.vars);
    expect(normalized["--spacing-symmetric-value"]).toBe("1.5rem");

    const normalizedTokens = normalizeResolvedTokens(result.resolvedTokens);
    expect(normalizedTokens.spacing.symmetric.value).toBe(
      "var(--spacing-symmetric-value)"
    );
  });
});

These test cases cover:

  1. Basic composite alias - Verifies the core bug fix where alias in resolved getter doesn't throw
  2. Cross-composite alias - Tests aliasing between different composite objects
  3. Mixed alias/non-alias - Ensures alias and normal references can coexist in one composite
  4. Nested composites - Validates alias works at multiple nesting levels
  5. Raw + alias combination - Tests that raw() and alias() can be used together
  6. Self-referential alias - Edge case where composite aliases its own properties

Each test validates both the CSS variable generation and the resolved token structure to ensure correctness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, I'am only considering it within the get method. I'll be building a separate behavior for compositeValue, which separates the scope.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@black7375, thank you for clarifying your approach! I understand you're planning to handle compositeValue with a separate behavior that has its own scope separation, rather than addressing it within the current getter-based flow.

That makes sense architecturally—separating concerns between regular getter-based semantic tokens and composite values with their own evaluation context. This would likely provide cleaner semantics and avoid the conflicts between accessor properties and value assignments.

If you'd like, I can help when you're ready to implement the separate compositeValue behavior. For example, I could:

  • Review the implementation once you've sketched it out
  • Help generate test cases for the new separate scope behavior
  • Validate that the new approach handles the alias/raw/fallbackVar utilities correctly within the composite context

Just let me know when you're working on that part, and feel free to tag me if you'd like input on the design or implementation! 🚀

@black7375
Copy link
Contributor Author

/fast-forward

@github-actions
Copy link
Contributor

Triggered from #277 (comment) by @​black7375.

Trying to fast forward main (26546b1) to theme-bug-fixes (236463b).

Target branch (main):

commit 26546b1a08cecbbaba4a4895d09657bdc7e7a633 (HEAD -> main, origin/main)
Author: alstjr7375 <alstjr7375@daum.net>
Date:   Thu Oct 16 00:00:00 2025 +0900

    Chore: update eslint config as consist

Pull request (theme-bug-fixes):

commit 236463ba62ad15c5ebb0bda3dc89e9d1e36e4eff (pull_request/theme-bug-fixes)
Author: alstjr7375 <alstjr7375@daum.net>
Date:   Sat Oct 18 00:00:00 2025 +0900

    Fix: `theme` - composite value

Fast forwarding main (26546b1) to theme-bug-fixes (236463b).

$ git push origin 236463ba62ad15c5ebb0bda3dc89e9d1e36e4eff:main
To https://github.com/mincho-js/mincho.git
   26546b1..236463b  236463ba62ad15c5ebb0bda3dc89e9d1e36e4eff -> main

@github-actions github-actions bot merged commit 236463b into main Nov 11, 2025
12 checks passed
@github-actions github-actions bot deleted the theme-bug-fixes branch November 11, 2025 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant