Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transforms that run after all references have been resolved #1063

Open
jorenbroekema opened this issue Dec 7, 2023 · 2 comments
Open

Transforms that run after all references have been resolved #1063

jorenbroekema opened this issue Dec 7, 2023 · 2 comments

Comments

@jorenbroekema
Copy link
Collaborator

jorenbroekema commented Dec 7, 2023

I'd like to propose adding a property to transforms that indicates they ought to be ran after references have been resolved entirely.

This means that transforms can be ran in 3 different ways (in chronological order), rather than only 2:

  • default, which is that the transforms run before any reference resolving has happened, and they only apply to token values that do not contain references
  • transitive true, which means that the transform puts properties with references in a deferred state, after which goes into a repetitive cycle of resolving the reference for such properties and attempting to transform it once again. If the resolved value is another chained reference, this cycle continues until the resolved value is not a reference, after which the transitive transform is finally applied to it. See example below.
  • (proposing to add) postTransitive true which means this transform is ran at the very end when all references have been resolved and all regular and transitive transforms have done their jobs.

Example transitive transform

Imagine the tokens below and a transitive transform that transforms the token value by a darken value, to darken the color.

{
  "color": {
    "red": { "value": "#f00" },
    "danger": { "value": "{color.red}", "darken": 0.75 },
    "error": { "value": "{color.danger}", "darken": 0.5 }
  }
}

In precise detail, events happen in the following order

  1. red is "transformed", but there is no "darken" so nothing happens.
  2. danger is skipped because it has a reference, but the property is added to deferred props for later transformation ["color.danger"].
  3. error is skipped because it has a reference, but the property is added to deferred props for later transformation ["color.danger", "color.error"].
  4. Deferred props ["color.danger", "color.error"] are added to exclusion list for referencing, so any value that is a reference to these, we need to hold off because we first need to apply transitive transforms to our first layer of refs, a reference to {color.red} for example.
  5. Resolving all references with the exception of ignorelist, which means {color.red} is resolved to #f00, but color.error.value which has {color.danger} is skipped for now, because that one is in the ignorelist.
  6. Apply transforms again, skipping tokens that were already transformed, but this time color.danger value is not a reference anymore. the original value is, so we only apply transitive transforms, meaning the value is darkened by 0.75.
  7. error is skipped again because it still has a reference, so it's still a deferred prop for later transformation ["color.error"].
  8. since we still have deferred prop ["color.error"], we do another resolve references call but this time the ignorelist only contains color.error because color.danger does not use a reference any longer.
  9. this means that reference color.danger within color.error is resolved to whatever is the current value of color.danger, which is the #00 but by now it is darkened by 0.75.
  10. Apply transforms again, skipping tokens that were already transformed, but this time color.error value is not a reference anymore. the original value is, so we only apply transitive transforms, meaning the value is darkened by 0.5.
  11. We do a final call of resolving references, this time with no deferred props and so, an empty ignore list, but it doesn't really matter since all references are already resolved at this point, so this call is mostly redundant.
  12. Because there are no deferred props, we are finished 🙂.

Example post-transitive transform

Input is the same:

{
  "color": {
    "red": { "value": "#f00" },
    "danger": { "value": "{color.red}", "darken": 0.75 },
    "error": { "value": "{color.danger}", "darken": 0.5 }
  }
}

However, the output is different, we now want:

import SwiftUI

public class  {
    public static let colorRed = UIColor(red: 255, green: 0, blue: 0, alpha: 1);
    public static let colorDanger = UIColor(red: 63.75, green: 0, blue: 0, alpha: 1);
    public static let colorError = UIColor(red: 31.88, green: 0, blue: 0, alpha: 1);
}

Which means we have the transitive transform for the darken color modifier running, but we also have a transform running that needs to transform the format into swift UIColor format.

The problem is that we cannot at this time add a transform that runs after the color modifications have ran, so what happens now is:

  1. color.red is transformed to UIColor format
  2. color.danger is deferred because it contains a reference, --> also added to ignorelist
  3. color.error is deferred because it contains a reference --> also added to ignorelist
  4. color.danger's reference to color.red is resolved, but at this point this is UIColor(red: 255, green: 0, blue: 0, alpha: 1)
  5. transitive transform color modifier is applied to color.danger but this color modifier doesn't understand UIColor format :( fatal error!
  6. color.error's reference to color.danger would be skipped due to ignorelist, would only happen in the next iteration if step 5 didn't fail. And this would then fail for the same reason.

Hopefully this makes clear the use case for having post-transitive transforms.

@jorenbroekema
Copy link
Collaborator Author

Maybe the prop can be called deferred: true|false btw, i think that's better than postTransitive

@jorenbroekema
Copy link
Collaborator Author

jorenbroekema commented Dec 8, 2023

Here's another example that showcases the need to defer transformation until after all references are resolved when trying to build a transform that wraps math expressions inside calc().

{
  "dimension": {
    "scale": {
      "value": "2",
      "type": "sizing"
    },
    "xs": {
      "value": "4px",
      "type": "sizing"
    },
    "sm": {
      "value": "{dimension.xs} * {dimension.scale}",
      "type": "sizing"
    },
    "md": {
      "value": "{dimension.sm} * {dimension.scale}",
      "type": "sizing"
    },
    "lg": {
      "value": "{dimension.md} * {dimension.scale}",
      "type": "sizing"
    },
  }
}
StyleDictionary.registerTransform({
  type: `value`,
  transitive: true,
  name: `figma/calc`,
  matcher: ({ value }) => typeof value === 'string' && value.includes('*') && !value.includes('calc('),
  transformer: ({ value }) => `calc(${value})`,
});

Expected output:

:root {
  --sd-dimension-scale: 2;
  --sd-dimension-xs: 4;
  --sd-dimension-sm: calc(4px * 2);
  --sd-dimension-md: calc(4px * 2 * 2);
  --sd-dimension-lg: calc(4px * 2 * 2 * 2);
}

Actual output:

:root {
  --sd-dimension-scale: 2px;
  --sd-dimension-xs: 4px;
  --sd-dimension-sm: calc(4px * 2px);
  --sd-dimension-md: calc(4px * 2px) * 2px;
  --sd-dimension-lg: calc(4px * 2px) * 2px * 2px;
}

Which is easy to explain when you understand the lifecycle of transitive transforms:

  1. sm, md and lg are all deferred
  2. in the first cycle, sm is resolved -> 4px * 2
  3. sm is then transformed -> calc(4px * 2)
  4. next cycle: md and lg are deferred
  5. md is resolved -> calc(4px * 2) * 2
  6. md is not transformed because it already has calc(), otherwise we would get: calc(calc(4px * 2) * 2)

There's a solution where we can always apply the transform even if it already has a calc statement, after which we get:

:root {
  --sd-dimension-scale: 2;
  --sd-dimension-xs: 4px;
  --sd-dimension-sm: calc(4px * 2);
  --sd-dimension-md: calc(calc(4px * 2) * 2);
  --sd-dimension-lg: calc(calc(calc(4px * 2) * 2) * 2);
}

Which is actually valid CSS, but it just contains a nested calc statement for every chain of reference, which is a bit bloated..

When we allow this transform to be deferred at the end, we can get the expected outcome instead, which I think is the best solution.

benelan pushed a commit to Esri/calcite-design-system that referenced this issue Dec 19, 2023
#8415)

[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@tokens-studio/sd-transforms](https://togithub.com/tokens-studio/sd-transforms)
| [`0.12.1` ->
`0.12.2`](https://renovatebot.com/diffs/npm/@tokens-studio%2fsd-transforms/0.12.1/0.12.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@tokens-studio%2fsd-transforms/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@tokens-studio%2fsd-transforms/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@tokens-studio%2fsd-transforms/0.12.1/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tokens-studio%2fsd-transforms/0.12.1/0.12.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>tokens-studio/sd-transforms
(@&#8203;tokens-studio/sd-transforms)</summary>

###
[`v0.12.2`](https://togithub.com/tokens-studio/sd-transforms/blob/HEAD/CHANGELOG.md#0122)

[Compare
Source](https://togithub.com/tokens-studio/sd-transforms/compare/v0.12.1...v0.12.2)

##### Patch Changes

-
[`7dad579`](https://togithub.com/tokens-studio/sd-transforms/commit/7dad579):
Workaround fix in color modifiers transform to allow UIColor format.
This workaround should be removed (in a breaking change) if
[amzn/style-dictionary#1063
gets resolved and post-transitive transforms become a thing.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 5am every weekday" in timezone
America/Los_Angeles, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/Esri/calcite-design-system).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy44Ny4yIiwidXBkYXRlZEluVmVyIjoiMzcuODcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant