From 57c81807d8fd28eb6214a4c57fe7845160af6571 Mon Sep 17 00:00:00 2001 From: nicholasrice Date: Fri, 30 Aug 2024 15:33:24 -0700 Subject: [PATCH 1/5] implement derived function --- src/lib/library.ts | 35 +++++++++++++++++--- src/test/library.spec.ts | 48 ++++++++++++++++------------ src/test/my-design-system/borders.ts | 12 +++---- src/test/my-design-system/colors.ts | 4 +-- src/test/my-design-system/fonts.ts | 4 ++- 5 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/lib/library.ts b/src/lib/library.ts index b7b71bb..361b92a 100644 --- a/src/lib/library.ts +++ b/src/lib/library.ts @@ -37,14 +37,37 @@ export namespace Library { : never; }; + export const DerivedSymbol = Symbol.for("design-token-library::derived"); + /** * A token value that serves as an alias to another token value * * @public */ - export type Alias> = ( - context: R - ) => T | DesignToken.ValueByToken; + export type DerivedSource< + T extends DesignToken.Any, + R extends Context + > = (context: R) => T | DesignToken.ValueByToken; + export type Alias< + T extends DesignToken.Any, + R extends Context + > = DerivedSource & { [DerivedSymbol]: typeof DerivedSymbol }; + + export function derive>( + value: DerivedSource + ): Alias { + if (isAlias(value)) { + return value; + } + + Reflect.defineProperty(value, DerivedSymbol, { + value: DerivedSymbol, + enumerable: false, + configurable: false, + }); + + return value as Alias; + } /** * An {@link (Library:namespace).Alias} that supports complex token value types @@ -150,7 +173,11 @@ const isGroup = ( const isAlias = ( value: any ): value is Library.Alias => { - return typeof value === "function"; + try { + return Reflect.get(value, Library.DerivedSymbol) === Library.DerivedSymbol; + } catch (e) { + return false; + } }; const recurseCreate = ( diff --git a/src/test/library.spec.ts b/src/test/library.spec.ts index eabad5b..aaf242e 100644 --- a/src/test/library.spec.ts +++ b/src/test/library.spec.ts @@ -150,7 +150,9 @@ Value("should invoke function values with the token library", () => { anotherToken: DesignToken.Color; } - const value = spy((context: Library.Context) => context.token); + const watched = spy((context: Library.Context) => context.token); + const value = Library.derive(watched); + const config: Library.Config = { token: { type: DesignToken.Type.Color, @@ -166,8 +168,8 @@ Value("should invoke function values with the token library", () => { // Act const anotherTokenValue = library.tokens.anotherToken.value; - Assert.equal(value.calledOnce, true); - Assert.equal(value.firstCall.args[0], library.tokens); + Assert.equal(watched.calledOnce, true); + Assert.equal(watched.firstCall.args[0], library.tokens); }); Value( @@ -184,7 +186,7 @@ Value( }, anotherToken: { type: DesignToken.Type.Color, - value: (theme: Theme) => theme.token, + value: Library.derive((theme) => theme.token), }, }; const library = Library.create(config); @@ -206,11 +208,11 @@ Value("reference tokens should support multiple levels of inheritance", () => { }, secondaryToken: { type: DesignToken.Type.Color, - value: (theme) => theme.token, + value: Library.derive((theme) => theme.token), }, tertiaryToken: { type: DesignToken.Type.Color, - value: (theme) => theme.secondaryToken, + value: Library.derive((theme) => theme.secondaryToken), }, }; const library = Library.create(config); @@ -237,10 +239,10 @@ Value("should support reading alias values from complex values", () => { border: { type: DesignToken.Type.Border, value: { - color: (context) => context.color, + color: Library.derive((context) => context.color), width: "3px", style: { - dashArray: [(context) => context.dimension, "14px"], + dashArray: [Library.derive((context) => context.dimension), "14px"], lineCap: "butt", }, }, @@ -295,7 +297,11 @@ Value("should support setting a token alias", () => { }; const library = Library.create(config); - library.tokens.secondaryToken.set((theme) => theme.token.value); + library.tokens.secondaryToken.set( + Library.derive>( + (theme) => theme.token.value + ) + ); Assert.equal(library.tokens.secondaryToken.value, library.tokens.token.value); }); @@ -318,7 +324,9 @@ Value("should support setting a value alias", () => { }; const library = Library.create(config); - library.tokens.secondaryToken.set(() => "#FF0000"); + library.tokens.secondaryToken.set( + Library.derive>(() => "#FF0000") + ); Assert.equal(library.tokens.secondaryToken.value, "#FF0000"); }); @@ -338,7 +346,7 @@ Value( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; @@ -367,13 +375,13 @@ Value( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, c: { type: DesignToken.Type.Border, value: { style: "solid", - color: (context) => context.b, + color: Library.derive((context) => context.b), width: "2px", }, }, @@ -544,7 +552,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -565,7 +573,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -585,7 +593,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -614,7 +622,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -647,7 +655,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -682,7 +690,7 @@ Extend( }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; const source = Library.create(config); @@ -713,7 +721,7 @@ Extend("Should allow adding new tokens to an extending library", async () => { }, b: { type: DesignToken.Type.Color, - value: (context) => context.a, + value: Library.derive((context) => context.a), }, }; interface Extending { diff --git a/src/test/my-design-system/borders.ts b/src/test/my-design-system/borders.ts index 983517a..9acb60d 100644 --- a/src/test/my-design-system/borders.ts +++ b/src/test/my-design-system/borders.ts @@ -12,18 +12,18 @@ export const borders: Library.Config = { type: DesignToken.Type.Border, accentThin: { value: { - color: function (theme) { + color: Library.derive(function (theme) { return theme.colors.accent; - }, + }), style: "dashed", - width(theme) { + width: Library.derive((theme) => { return theme.dimensions.border; - }, + }), }, }, neutralThin: { - value: function (theme): DesignToken.Border { + value: Library.derive(function (theme): DesignToken.Border { return theme.borders.accentThin; - }, + }), }, }; diff --git a/src/test/my-design-system/colors.ts b/src/test/my-design-system/colors.ts index 3badc02..47fa03d 100644 --- a/src/test/my-design-system/colors.ts +++ b/src/test/my-design-system/colors.ts @@ -14,8 +14,8 @@ export const colors: Library.Config = { value: "#FFFFFF", }, accent: { - value: function (theme) { + value: Library.derive(function (theme) { return theme.colors.neutral; - }, + }), }, }; diff --git a/src/test/my-design-system/fonts.ts b/src/test/my-design-system/fonts.ts index 6c65c9d..4d81760 100644 --- a/src/test/my-design-system/fonts.ts +++ b/src/test/my-design-system/fonts.ts @@ -13,7 +13,9 @@ export interface Fonts { export const fonts: Library.Config = { body: { value: ["foo", "bar"] }, - heading: { value: ["bat", (theme) => theme.fonts.body] }, + heading: { + value: ["bat", Library.derive((theme) => theme.fonts.body)], + }, weights: { normal: { value: "normal" }, heavy: { value: "heavy" }, From 236d29ccabb987c785a3d0a268c212c75c4f9586 Mon Sep 17 00:00:00 2001 From: nicholasrice Date: Mon, 2 Sep 2024 21:08:48 -0700 Subject: [PATCH 2/5] rename DeepAlias --- src/lib/library.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib/library.ts b/src/lib/library.ts index 361b92a..7befea2 100644 --- a/src/lib/library.ts +++ b/src/lib/library.ts @@ -48,14 +48,14 @@ export namespace Library { T extends DesignToken.Any, R extends Context > = (context: R) => T | DesignToken.ValueByToken; - export type Alias< + export type Derived< T extends DesignToken.Any, R extends Context > = DerivedSource & { [DerivedSymbol]: typeof DerivedSymbol }; export function derive>( value: DerivedSource - ): Alias { + ): Derived { if (isAlias(value)) { return value; } @@ -66,7 +66,7 @@ export namespace Library { configurable: false, }); - return value as Alias; + return value as Derived; } /** @@ -75,12 +75,12 @@ export namespace Library { * * @public */ - export type DeepAlias< + export type DeepDerived< V extends DesignToken.Values.Any, T extends Context > = { [K in keyof V]: V[K] extends DesignToken.Values.Any - ? V[K] | Alias, T> | DeepAlias + ? V[K] | Derived, T> | DeepDerived : never; }; @@ -105,7 +105,7 @@ export namespace Library { * @public */ export type Token = { - set(value: DesignToken.ValueByToken | Alias): void; + set(value: DesignToken.ValueByToken | Derived): void; toString(): string; readonly type: DesignToken.TypeByToken; readonly extensions: Record; @@ -137,8 +137,8 @@ export namespace Library { // in Library.create is untyped, it cannot be inferred, so use T | ... | (Omit & { value: - | Library.Alias> - | Library.DeepAlias, Context>; + | Library.Derived> + | Library.DeepDerived, Context>; }); /** @@ -172,7 +172,7 @@ const isGroup = ( const isAlias = ( value: any -): value is Library.Alias => { +): value is Library.Derived => { try { return Reflect.get(value, Library.DerivedSymbol) === Library.DerivedSymbol; } catch (e) { @@ -414,16 +414,16 @@ class LibraryImpl implements Library.Library { class LibraryToken implements Library.Token, - ISubscriber>, + ISubscriber>, IWatcher { - private raw: DesignToken.ValueByToken | Library.Alias; + private raw: DesignToken.ValueByToken | Library.Derived; private cached: DesignToken.ValueByToken | typeof empty = empty; private subscriptions: Set> = new Set(); constructor( public readonly name: string, - value: DesignToken.ValueByToken | Library.Alias, + value: DesignToken.ValueByToken | Library.Derived, private readonly _type: DesignToken.TypeByToken, private readonly context: Library.Context, private readonly _description: string, @@ -469,7 +469,7 @@ class LibraryToken return value; } - public set(value: DesignToken.ValueByToken | Library.Alias) { + public set(value: DesignToken.ValueByToken | Library.Derived) { this.raw = value; this.onChange(); } From ea5f94dd9c9db3bfee582ea526090deb4c011643 Mon Sep 17 00:00:00 2001 From: nicholasrice Date: Mon, 2 Sep 2024 21:13:02 -0700 Subject: [PATCH 3/5] update tests and comments --- src/lib/library.ts | 20 ++++++++++---------- src/test/library.spec.ts | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lib/library.ts b/src/lib/library.ts index 7befea2..49a9368 100644 --- a/src/lib/library.ts +++ b/src/lib/library.ts @@ -40,7 +40,7 @@ export namespace Library { export const DerivedSymbol = Symbol.for("design-token-library::derived"); /** - * A token value that serves as an alias to another token value + * A token value that derives a value from context * * @public */ @@ -56,7 +56,7 @@ export namespace Library { export function derive>( value: DerivedSource ): Derived { - if (isAlias(value)) { + if (isDerived(value)) { return value; } @@ -70,7 +70,7 @@ export namespace Library { } /** - * An {@link (Library:namespace).Alias} that supports complex token value types + * An {@link (Library:namespace).Derived} that supports complex token value types * such as {@link DesignToken.Border} * * @public @@ -85,7 +85,7 @@ export namespace Library { }; /** - * Context object provided to {@link (Library:namespace).Alias} values at runtime + * Context object provided to {@link (Library:namespace).Derived} values at runtime * * @public */ @@ -170,7 +170,7 @@ const isGroup = ( return isObject(value) && !isToken(value); }; -const isAlias = ( +const isDerived = ( value: any ): value is Library.Derived => { try { @@ -228,8 +228,8 @@ const recurseCreate = ( ); Reflect.defineProperty(library, key, { get() { - // Token access needs to be tracked because an alias token - // is a function that returns a token + // Token access needs to be tracked because derived values + // are a function that returns a token Watcher.track(token); return token; }, @@ -309,7 +309,7 @@ const recurseExtend = ( ); Reflect.defineProperty(extendedTokens, key, { get() { - // Token access needs to be tracked because an alias token + // Token access needs to be tracked because a derived value // is a function that returns a token Watcher.track(token); return token; @@ -360,7 +360,7 @@ const recurseResolve = (value: any, context: Library.Context) => { for (const key in value) { let v = value[key]; - if (isAlias(v)) { + if (isDerived(v)) { v = v(context); } @@ -456,7 +456,7 @@ class LibraryToken this.disconnect(); const stopWatching = Watcher.use(this); - const raw = isAlias(this.raw) ? this.raw(this.context) : this.raw; + const raw = isDerived(this.raw) ? this.raw(this.context) : this.raw; const normalized = isToken(raw) ? raw.value : raw; const value = isObject(normalized) diff --git a/src/test/library.spec.ts b/src/test/library.spec.ts index aaf242e..6c4b2ad 100644 --- a/src/test/library.spec.ts +++ b/src/test/library.spec.ts @@ -220,7 +220,7 @@ Value("reference tokens should support multiple levels of inheritance", () => { Assert.equal(library.tokens.tertiaryToken.value, library.tokens.token.value); }); -Value("should support reading alias values from complex values", () => { +Value("should support reading derived values from complex values", () => { interface Theme { color: DesignToken.Color; dimension: DesignToken.Dimension; @@ -252,12 +252,12 @@ Value("should support reading alias values from complex values", () => { const library = Library.create(config); const border = library.tokens.border.value; - Assert.equal(border.color, "#FF0000", "color alias should be equal"); + Assert.equal(border.color, "#FF0000", "derived color should be equal"); Assert.equal(border.width, "3px", "dimension value should be equal"); Assert.equal( border.style, { dashArray: ["12px", "14px"], lineCap: "butt" }, - "DeepAlias border style should be equal" + "DeepDerived border style should be equal" ); }); @@ -279,7 +279,7 @@ Value("should support setting a static value", () => { Assert.equal(library.tokens.token.value, value); }); -Value("should support setting a token alias", () => { +Value("should support setting a derived token", () => { interface Theme { token: DesignToken.Color; secondaryToken: DesignToken.Color; @@ -306,7 +306,7 @@ Value("should support setting a token alias", () => { Assert.equal(library.tokens.secondaryToken.value, library.tokens.token.value); }); -Value("should support setting a value alias", () => { +Value("should support setting a derived value", () => { interface Theme { token: DesignToken.Color; secondaryToken: DesignToken.Color; @@ -332,7 +332,7 @@ Value("should support setting a value alias", () => { }); Value( - "should update the value of a token assigned an alias after the alias value changes", + "should update the value of a token assigned an derived fn after the derived fn value changes", () => { interface Theme { a: DesignToken.Color; @@ -360,7 +360,7 @@ Value( ); Value( - "should update the value of a token assigned a value alias after the alias value changes", + "should update the value of a token assigned a derived value after the derived value changes", () => { interface Theme { a: DesignToken.Color; From 648a7f6664e6ad54efe3c4a1ea5bde252b90df33 Mon Sep 17 00:00:00 2001 From: nicholasrice Date: Mon, 2 Sep 2024 21:46:44 -0700 Subject: [PATCH 4/5] remove un-necessary property assignment --- src/lib/library.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/library.ts b/src/lib/library.ts index 49a9368..e0a1d75 100644 --- a/src/lib/library.ts +++ b/src/lib/library.ts @@ -62,8 +62,6 @@ export namespace Library { Reflect.defineProperty(value, DerivedSymbol, { value: DerivedSymbol, - enumerable: false, - configurable: false, }); return value as Derived; From a51763b173be98ea03324bd071a795472e7cc93e Mon Sep 17 00:00:00 2001 From: nicholasrice Date: Tue, 3 Sep 2024 20:45:32 -0700 Subject: [PATCH 5/5] update documentation --- doc-site/docs/getting-started.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc-site/docs/getting-started.md b/doc-site/docs/getting-started.md index 215620b..7c30d9e 100644 --- a/doc-site/docs/getting-started.md +++ b/doc-site/docs/getting-started.md @@ -74,13 +74,13 @@ const myLibraryConfig: Library.Config = { ### Tokens -Each token in the library must have a `value` property, and groups must not have a value. Tokens can be assigned three types of values: **static**, **alias**, and **computed**. +Each token in the library must have a `value` property, and groups must not have a value. Tokens can be assigned three types of values: **static**, **alias**, and **derived**. ```ts interface Colors { static: DesignToken.Color; alias: DesignToken.Color; - computed: DesignToken.Color; + derived: DesignToken.Color; } const config: Library.Config = { @@ -92,7 +92,7 @@ const config: Library.Config = { type: DesignToken.Type.Color, value: (tokens) => tokens.static, // alias to the 'static' token }, - computed: { + derived: { type: DesignToken.Type.Color, // Operate on the value of the 'alias' token value: (tokens) => darken(tokens.alias.value, 0.3), @@ -106,7 +106,7 @@ In alignment with the [DTCG Group](https://design-tokens.github.io/community-gro ## Creating a Library -With the configuration defined, the library can be created. The purpose of the library is to enable changes to token values, notify subscribers to changes, and reconciling alias and computed values with those changes. +With the configuration defined, the library can be created. The purpose of the library is to enable changes to token values, notify subscribers to changes, and reconciling alias and derived values with those changes. ```ts const library = Library.create(myLibraryConfig); @@ -144,7 +144,7 @@ library.subscribe(subscriber); library.tokens.foreground.set("#878787"); ``` -Change notifications are batched and subscribers get notified each microtask. It's important to note that token values are lazily evaluated. If a computed or alias token has not been accessed, it will **not** notify itself to subscribers even if it's dependencies change: +Change notifications are batched and subscribers get notified each microtask. It's important to note that token values are lazily evaluated. If a derived token has not been accessed, it will **not** notify itself to subscribers even if it's dependencies change: ```ts const library = Library.create({