feat(tokens): cross-platform token-path ↔ token resolver#39
Conversation
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>
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Priva28
left a comment
There was a problem hiding this comment.
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.
| import NucleusButtons | ||
| import NucleusColors | ||
| import NucleusFonts | ||
| import NucleusIcons |
There was a problem hiding this comment.
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
TokenResolvableon 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
}
}| "typography.body.b1": .b1, | ||
| "typography.body.b2": .b2, |
There was a problem hiding this comment.
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.
| "icon.bell.outline": (.bell, .outline), | ||
| "icon.bell.regular": (.bell, .regular), | ||
| "icon.bell.solid": (.bell, .solid), |
There was a problem hiding this comment.
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>
|
@codex review |
There was a problem hiding this comment.
💡 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('.'), |
There was a problem hiding this comment.
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>
|
@codex review |
There was a problem hiding this comment.
💡 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]] |
There was a problem hiding this comment.
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>
|
@codex review |
There was a problem hiding this comment.
💡 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('.'), |
There was a problem hiding this comment.
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 👍 / 👎.
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)NucleusTokenResolverSPM target:color(_:) -> NucleusColor?,font(_:) -> NucleusFont?,button(_:) -> NucleusButton?,icon(_:) -> (NucleusIcon, NucleusIcon.Variant)?NucleusTokenResolverobject:color(token, isDark) -> Color?,font(token) -> NucleusFontStyle?,button(token) -> NucleusButtonStyle?,iconRes(token) -> @DrawableRes Int?token-paths.{js,d.ts}: canonical path constants (literal-typedColorTokens/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
tokens/formats/resolver-{shared,web,ios,android}.ts+tokens/build/resolver.ts, wired intotokens/build/index.ts.loadColorTokens,loadFontDefinitions,resolveButtonStyles,discoverIconTokens,camelCasePath/publicColorPath,typographyTokenPath), so the resolver can't drift from the static accessors it points at.Coverage / safety
android(:nucleus:check) andios(xcodebuild) jobs — a wrong symbol reference fails CI.npm run buildis idempotent;typecheck/lint/format:checkpass.Follow-ups (out of this PR)
X.Y.0) so@worldcoin/nucleuspublishestoken-paths(web) + the SPM/Maven resolver.token-pathsand deletes its hand-written token catalogs.NucleusTokenResolverwith 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