Skip to content

feat(tokens): cross-platform token-path ↔ token resolver#39

Merged
Gr1dlock merged 6 commits into
mainfrom
feat/sdui-token-resolver
Jun 16, 2026
Merged

feat(tokens): cross-platform token-path ↔ token resolver#39
Gr1dlock merged 6 commits into
mainfrom
feat/sdui-token-resolver

Conversation

@Gr1dlock

Copy link
Copy Markdown
Contributor

What

Generates a token-path ↔ token resolver so consumers can turn the canonical Nucleus path strings (the format emitted on the SDUI wire, e.g. semantic.color.text.primary, component.button.inverse.32, typography.subtitle.s3, icon.arrow-right.regular) back into native tokens. Today the native packages expose tokens only as compile-time static accessors with no string→token resolver, so each client would hand-roll the mapping (with three different per-family transforms). This moves that mapping into Nucleus.

Outputs (all generated from tokens/definitions/*.json)

  • iOS — new NucleusTokenResolver SPM target:
    color(_:) -> NucleusColor?, font(_:) -> NucleusFont?, button(_:) -> NucleusButton?, icon(_:) -> (NucleusIcon, NucleusIcon.Variant)?
  • AndroidNucleusTokenResolver object:
    color(token, isDark) -> Color?, font(token) -> NucleusFontStyle?, button(token) -> NucleusButtonStyle?, iconRes(token) -> @DrawableRes Int?
  • Webtoken-paths.{js,d.ts}: canonical path constants (literal-typed ColorTokens / PrimitiveColorTokens / TypographyTokens / ButtonTokens / IconTokens). The app-backend SDUI library imports these instead of hand-writing path literals, so the backend maintains no mapping of its own.

Forward-only (string → token) per the agreed scope; the path constants cover the serialization direction.

How

  • New tokens/formats/resolver-{shared,web,ios,android}.ts + tokens/build/resolver.ts, wired into tokens/build/index.ts.
  • Reuses the existing path-builders and accessor-naming (loadColorTokens, loadFontDefinitions, resolveButtonStyles, discoverIconTokens, camelCasePath/publicColorPath, typographyTokenPath), so the resolver can't drift from the static accessors it points at.
  • A generation-time guard rejects duplicate/empty paths or keys.

Coverage / safety

  • Resolves the full published set: 101 colors (49 semantic + 52 primitive), 21 typography, 18 buttons, 416 icons.
  • The committed iOS/Android files are covered by the existing "generated files in sync" CI check and compiled by the android (:nucleus:check) and ios (xcodebuild) jobs — a wrong symbol reference fails CI.
  • npm run build is idempotent; typecheck / lint / format:check pass.

Follow-ups (out of this PR)

  • Cut a release (X.Y.0) so @worldcoin/nucleus publishes token-paths (web) + the SPM/Maven resolver.
  • app-backend SDUI V4 (PR #8313) then imports token-paths and deletes its hand-written token catalogs.
  • World App clients adopt NucleusTokenResolver with an unknown-token fallback + telemetry + a Nucleus version floor (handles server/client version skew — the resolver guarantees intra-version consistency, not cross-process skew).

🤖 Generated with Claude Code

Gr1dlock and others added 3 commits June 15, 2026 17:06
Adds a generated resolver that maps the canonical Nucleus token-path strings
(as emitted on the SDUI wire) to native tokens, so consumers no longer hand-roll
the mapping:

- iOS: NucleusTokenResolver (new SPM target) — string → NucleusColor / NucleusFont
  / NucleusButton / (NucleusIcon, Variant)
- Android: NucleusTokenResolver — string → Color (theme-aware) / NucleusFontStyle /
  NucleusButtonStyle / @DrawableRes
- Web: token-paths.{js,d.ts} — canonical path constants (literal-typed) for the
  app-backend SDUI library to import instead of hand-writing path literals

Generated from the same tokens/definitions/*.json that drives the static accessors
(reuses loaders + path-builders), so the resolver cannot drift; the iOS/Android
files are committed and covered by the existing "generated files in sync" CI check
and the android/ios build jobs. Forward-only (string → token).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Split color resolution into single-param colorLight/colorDark `when`s behind a
block-body color(token, isDark) dispatcher, so no branch exceeds the 120-char
limit and the 2-param signature no longer trips the function-signature rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…solver

Wrap the 2-param color() signature (params on own lines + trailing comma) with an
expression body, and move each multiline `when` body onto its own line, per the
repo's enabled function-signature rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Gr1dlock

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 91d07304f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Gr1dlock Gr1dlock requested review from Priva28 and jaidensiu June 15, 2026 18:11

@Priva28 Priva28 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good direction, and very similar to what I was trying to achieve here: https://github.com/worldcoin/nucleus/pull/38/changes

Would like to have some changes on the iOS side though. Let me know if you want me to commit to your branch.

Comment on lines +3 to +6
import NucleusButtons
import NucleusColors
import NucleusFonts
import NucleusIcons

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Having a separate target that imports all other modules creates tight coupling between them that we should avoid. If a part of the app only needs colors but wants to resolve a token, they would then also be bringing in all of the font and icon assets.

I'd say we structure it like this:

NucleusTokens/TokenResolvable.swift:

public protocol TokenResolvable {
    associatedtype ResolvedType
    static func resolve(token: String) -> ResolvedType?
}
  • Each target then implements TokenResolvable on it's primitive

NucleusFont/NucleusFont+TokenResolvable.swift:

private let tokenDefaults: [String: NucleusFont] = [
    "b2": .b2
    ...
]

extension NucleusFont: TokenResolvable {
    public static func resolve(token: String) -> NucleusFont? {
        guard let font = tokenDefaults[token] else {
            assertionFailure("unknown token: \(token)")
            return nil
        }
        return font
    }
}

Comment on lines +132 to +133
"typography.body.b1": .b1,
"typography.body.b2": .b2,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These token names are superfluous. It should be fine to resolve implicitly based on the context. If we're resolving a font, we already know it's "typography". If the token begins with b we already know it's a "body" font.

"b2" -> NucleusFont.b2 is fine and will save bandwidth on the network requests.

Comment on lines +232 to +234
"icon.bell.outline": (.bell, .outline),
"icon.bell.regular": (.bell, .regular),
"icon.bell.solid": (.bell, .solid),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Each of these doesn't need an explicit map. We should map icon name, and then variant.

Expanding on my suggestion above we'd probably have an implementation of TokenResolvable like this:

private let iconTokens: [String: NucleusIcon] = [
    "bell": .bell,
    ...
]

private let variantTokens: [String: NucleusIcon.Variant] = [
    "outline": .outline,
    "regular": .regular,
    "solid": .solid
]

extension NucleusIcon: TokenResolvable {
    public static func resolve(token: String) -> (NucleusIcon, NucleusIcon.Variant)? {
        let components = token.components(separatedBy: ".")
        guard components.count == 2 else {
            assertionFailure("icon token must have 2 components")
            return nil
        }

        guard
            let icon = iconTokens[components[0]],
            let variant = variantTokens[components[1]]
        else {
            assertionFailure("unknown token: \(token)")
            return nil
        }

        return (icon, variant)
    }
}

…eview)

Address review on the resolver:
- Drop the monolithic NucleusTokenResolver iOS target (it imported every module).
  Add a zero-dependency NucleusTokens target with a TokenResolvable protocol; each
  primitive conforms in its own module via a generated +TokenResolvable.swift, so a
  consumer only links what it imports.
- Shorten wire tokens to the type-scoped form the resolver context implies:
  font `s3`, color `text.primary`, button `inverse.32`, icon `name.variant`
  (iOS icon resolve splits name + variant). Saves bandwidth on SDUI responses.
- Web token-paths constants + Android resolver updated to the same short tokens.

Generated from the same definitions as the static accessors; covered by the sync
check + the android/ios build jobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Gr1dlock

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42c496a027

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


function colorEntries(source: string): ColorEntry[] {
return loadColorTokens(source).map((leaf) => ({
wireToken: publicColorPath(leaf.path).join('.'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve full wire-token paths

This catalog field drives the published web constants and every native resolver key, but it strips the semantic.color/primitive.color namespace here and the later wireToken assignments similarly use bare s3, inverse.32, and arrow-right.regular. The existing web type generator defines the wire contract as canonical full paths (tokens/formats/types-web.ts:9-13 builds semantic.color.*, typography.*, component.button.*, and icon.*), so SDUI payloads or backend constants typed against those canonical values will miss these maps and return nil/null. Keep the full canonical path in wireToken and derive only the native accessor from the stripped public path.

Useful? React with 👍 / 👎.

…odex P1)

The literal-union types (ColorToken/TypographyToken/ButtonStyleToken/IconToken) still
emitted full canonical paths while token-paths + the resolvers moved to short type-scoped
tokens — two conflicting wire contracts in one package. Source the union members from the
same buildTokenCatalog() wireToken values that drive token-paths and the native resolvers,
so the types, the runtime constants, and the resolver keys are all in lockstep (short form).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Gr1dlock

Copy link
Copy Markdown
Contributor Author

@codex review

@jaidensiu jaidensiu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thanks @Priva28 for pointing things out on the iOS side!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a1301d41e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
guard
let icon = iconTokens[components[0]],
let variant = variantTokens[components[1]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unavailable icon variants

For icons that do not ship every variant, this only verifies the icon name and that the variant exists globally, so NucleusIcon.resolve(token: "antenna-signal.solid") returns non-nil even though NucleusIcon+Defaults.swift lists .antennaSignal as only [.outline, .regular] and assetName(for:) will then return nil. The resolver contract says unknown tokens should return nil/assert, and the Android/web resolvers only expose real name+variant pairs, so iOS accepts invalid SDUI icon tokens instead of falling back/reporting them. Check icon.availableVariants.contains(variant) or generate this lookup from catalog.icons before returning.

Useful? React with 👍 / 👎.

NucleusIcon.resolve checked only that the icon name and the variant existed globally,
so e.g. `antenna-signal.solid` resolved non-nil even though antenna-signal ships only
[outline, regular] (assetName(for:) would then return nil). Gate on the icon's own
availableVariants so an unsupported name.variant returns nil per the resolver contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Gr1dlock

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4cc96821d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


function colorEntries(source: string): ColorEntry[] {
return loadColorTokens(source).map((leaf) => ({
wireToken: publicColorPath(leaf.path).join('.'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve canonical token paths in resolver keys

For SDUI payloads that use the canonical paths described by the existing token contract, e.g. semantic.color.text.primary, this strips the semantic.color/primitive.color prefix before generating every native resolver and the web constants, so generated lookups only contain keys like text.primary and will return nil/null for backend-provided color tokens. The same catalog is also used below to strip typography, button, and icon namespaces, so either the resolver should key on canonical paths or explicitly accept both forms.

Useful? React with 👍 / 👎.

@Gr1dlock Gr1dlock merged commit 534ff6f into main Jun 16, 2026
9 checks passed
@Gr1dlock Gr1dlock deleted the feat/sdui-token-resolver branch June 16, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants