design-token-library 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,80 +1,407 @@
1
+ import { COMPUTED, RECIPE } from "./recipe.js";
2
+ import { REF } from "./ref.js";
1
3
  import { DesignToken } from "./design-token.js";
2
- import { DeepPartial } from "./utilities.js";
4
+ /**
5
+ * The key of the phantom type of a ref: what it resolves to, in a library of
6
+ * what shape. It's never set.
7
+ */
8
+ declare const RESOLVES: unique symbol;
9
+ /**
10
+ * The key of the phantom type of a computed value, apart from a ref's, so
11
+ * that a ref's type is never inferred from a computed value's.
12
+ */
13
+ declare const COMPUTES: unique symbol;
14
+ /**
15
+ * The key of the phantom type that marks a token in
16
+ * {@link (Library:namespace).TokenValues}. It's never set.
17
+ */
18
+ declare const TOKEN: unique symbol;
3
19
  /**
4
20
  * @public
5
21
  */
6
22
  export declare namespace Library {
7
- interface Library<T extends {}, R extends {} = T> {
23
+ export interface Library<T extends {}, R extends {} = T> {
8
24
  tokens: TokenLibrary<T, R>;
9
25
  subscribe(subscriber: Library.Subscriber<R>): void;
10
26
  unsubscribe(subscriber: Library.Subscriber<R>): void;
11
- extend<K extends {} = any>(config: DeepPartial<T> & Config<K, R & K>): Library<T & K, R & K>;
27
+ /**
28
+ * Creates a library with the values of the tokens replaced, and
29
+ * recipes overridden.
30
+ *
31
+ * @remarks
32
+ * This has no tokens to add, so a value is checked against the type
33
+ * of its token wherever the token is.
34
+ */
35
+ extend(config: ExtendConfig<T, R>, options?: ExtendOptions): Library<T, R>;
36
+ /**
37
+ * Creates a library with the values of tokens replaced, new tokens,
38
+ * and recipes overridden.
39
+ */
40
+ extend<K extends NoRoot<K> = {}>(config: ExtendConfig<T, R & NewTokens<K, T>> & Config<K, R & NewTokens<K, T>, never, T> & NoInfer<ConfigValueCheck<K, R & NewTokens<K, T>>> & (ConfigTypeHint | {}), options?: ExtendOptions): Library<T & NewTokens<K, T>, R & NewTokens<K, T>>;
12
41
  }
13
- interface Subscriber<R extends {}> {
14
- onChange(records: ReadonlyArray<Library.Token<DesignToken.Any, R>>): void;
42
+ /**
43
+ * The tokens and groups of `K` that are new relative to a source library
44
+ * of shape `T`. Overrides of tokens in `T` are removed so that they keep
45
+ * the source token's type.
46
+ *
47
+ * @public
48
+ */
49
+ export type NewTokens<K, T> = {
50
+ [P in keyof K as P extends keyof T ? T[P] extends DesignToken.Shape ? never : P : P]: P extends keyof T ? NewTokens<K[P], T[P]> : K[P];
51
+ };
52
+ /**
53
+ * `$root` is the token of a group, so a library cannot have one at its top
54
+ * level, where there is no group to name it for.
55
+ *
56
+ * @public
57
+ */
58
+ export type NoRoot<T> = {
59
+ [K in keyof T]: K extends "$root" ? never : T[K];
60
+ };
61
+ export interface Subscriber<R extends {}> {
62
+ onChange(records: ReadonlyArray<Library.TokenRecord<R>>): void;
15
63
  }
64
+ /**
65
+ * A union of all tokens in a library of shape `T`.
66
+ *
67
+ * @public
68
+ */
69
+ export type TokensOf<T> = {
70
+ [K in keyof T]-?: T[K] extends DesignToken.Shape ? T[K] : K extends keyof DesignToken.Group ? never : T[K] extends object ? TokensOf<T[K]> : never;
71
+ }[keyof T];
72
+ /**
73
+ * A {@link (Library:namespace).Token} of any token in a library of shape `R`.
74
+ * The union is discriminated by `type`.
75
+ *
76
+ * @public
77
+ */
78
+ export type TokenRecord<R extends {}> = TokensOf<R> extends infer T ? T extends DesignToken.Shape ? Token<T, R> : never : never;
16
79
  /**
17
80
  * Defines a token library that can be interacted with
18
81
  * to mutate token values.
19
82
  *
83
+ * @remarks
84
+ * `G` is the type inherited from ancestor groups.
85
+ *
86
+ * @public
87
+ */
88
+ export type TokenLibrary<T extends {}, R extends {} = T, G extends string = never> = TokenGroup<T, R, GroupType<T, G>>;
89
+ type TokenGroup<T extends {}, R extends {}, G extends string> = {
90
+ [K in keyof Readonly<T>]: T[K] extends DesignToken.Shape ? Token<T[K], R, G> : K extends keyof DesignToken.Group ? T[K] : T[K] extends {} ? TokenLibrary<T[K], R, G> : never;
91
+ };
92
+ /**
93
+ * The value type of each token of a library of shape `T`, by its path.
94
+ * Refs and computed values are typed by it, so they're given only to a
95
+ * library that has their tokens, with values of the same types, whether
96
+ * its shape is declared or inferred from its config.
97
+ *
98
+ * @public
99
+ */
100
+ export type TokenValues<T> = {
101
+ [K in keyof T as K extends keyof DesignToken.Group ? never : K]: T[K] extends DesignToken.Shape ? TokenValue<DesignToken.ValueByToken<T[K]>> : T[K] extends object ? TokenValues<T[K]> : never;
102
+ };
103
+ /**
104
+ * A token with a value of type `V`, in
105
+ * {@link (Library:namespace).TokenValues}. It's only a type: it marks
106
+ * where a path ends at a token rather than going into its value.
107
+ *
108
+ * @public
109
+ */
110
+ export interface TokenValue<V> {
111
+ readonly [TOKEN]: V;
112
+ }
113
+ /**
114
+ * The dotted path of each token in `T`, the
115
+ * {@link (Library:namespace).TokenValues} of a library, whose value is a
116
+ * `V`. These are the paths {@link ref} takes where a `V` is expected.
117
+ *
118
+ * @public
119
+ */
120
+ export type TokenPath<T, V> = {
121
+ [K in keyof T & string]: T[K] extends TokenValue<infer W> ? [W] extends [V] ? K : never : `${K}.${TokenPath<T[K], V>}`;
122
+ }[keyof T & string];
123
+ /**
124
+ * A reference to a token whose value is of type `V`, in a library whose
125
+ * token values are `T`. It's resolved to the token's value when the token
126
+ * that has it is read. Made by {@link ref} and {@link refs}.
127
+ *
128
+ * @remarks
129
+ * A ref is data: the path of its token. `T`, the
130
+ * {@link (Library:namespace).TokenValues} of the library, is only in its
131
+ * type, so that a ref can only be given to a library that has its token.
132
+ *
133
+ * @public
134
+ */
135
+ export interface Ref<V, T> {
136
+ readonly [REF]: readonly string[];
137
+ readonly [RESOLVES]?: (library: T) => V;
138
+ }
139
+ /**
140
+ * A value of type `V` computed by a {@link (Library:namespace).Recipe}
141
+ * from its arguments, which may refer to the tokens of a library whose
142
+ * {@link (Library:namespace).TokenValues} are `T`. It's computed when the
143
+ * token that has it is read.
144
+ *
145
+ * @remarks
146
+ * A computed value is data: the recipe and its arguments.
147
+ *
148
+ * @public
149
+ */
150
+ export interface Computed<V, T> {
151
+ readonly [COMPUTED]: {
152
+ readonly recipe: Recipe<any, unknown>;
153
+ readonly args: readonly unknown[];
154
+ };
155
+ readonly [COMPUTES]?: (library: T) => V;
156
+ }
157
+ /**
158
+ * A value of type `V` in a configuration: the value, a
159
+ * {@link (Library:namespace).Ref}, a {@link (Library:namespace).Computed}
160
+ * value, or, when `V` is an object or an array, `V` with any of its
161
+ * properties and elements, at any depth, given as one of those.
162
+ *
163
+ * @remarks
164
+ * Its objects and arrays may be readonly, as a token's value is, so a
165
+ * token's value can be given where a config takes a value, as `set()`
166
+ * takes it. A computed value may be readonly, as a recipe's function that
167
+ * takes readonly arguments returns: the library copies and freezes a
168
+ * recipe's result.
169
+ *
20
170
  * @public
21
171
  */
22
- type TokenLibrary<T extends {}, R extends {} = T> = {
23
- [K in keyof Readonly<T>]: T[K] extends DesignToken.Any ? Token<T[K], R> : K extends "type" ? DesignToken.Type : T[K] extends {} ? TokenLibrary<T[K], R> : never;
172
+ export type DeepValue<V, T> = V | Ref<V, T> | Computed<DesignToken.DeepReadonly<V>, T> | (V extends object ? {
173
+ readonly [K in keyof V]: DeepValue<V[K], T>;
174
+ } : never);
175
+ /**
176
+ * The arguments of a {@link (Library:namespace).Recipe} whose function has
177
+ * the parameters `A`. Each may be a ref, a computed value, or a value with
178
+ * them in it.
179
+ *
180
+ * @public
181
+ */
182
+ export type RecipeArguments<A extends readonly unknown[], T> = {
183
+ [K in keyof A]: DeepValue<A[K], T>;
24
184
  };
25
185
  /**
26
- * A token value that serves as an alias to another token value
186
+ * A function, made by {@link recipes}, that takes the arguments of a
187
+ * function with parameters `A` and result `V`, any of which may refer to
188
+ * tokens, and returns the {@link (Library:namespace).Computed} result.
189
+ * `T` is the token values of the library its arguments refer to.
190
+ *
191
+ * @remarks
192
+ * A recipe has its name and its function, which a library that extends
193
+ * another can override with {@link override}. The function gets each
194
+ * argument as a token gets its value: a ref's value, and a copy of plain
195
+ * data, which are frozen, and anything else as it is. So its parameters
196
+ * are typed readonly, as in `DesignToken.Values.Readonly.Color`.
27
197
  *
28
198
  * @public
29
199
  */
30
- type Alias<T extends DesignToken.Any, R extends Context<any>> = (context: R) => T | DesignToken.ValueByToken<T>;
200
+ export type Recipe<A extends readonly unknown[], V> = (<T extends {} = {}>(...args: RecipeArguments<A, T>) => Computed<V, T>) & {
201
+ readonly [RECIPE]: {
202
+ readonly name: string;
203
+ readonly fn: (...args: A) => V;
204
+ };
205
+ };
31
206
  /**
32
- * An {@link (Library:namespace).Alias} that supports complex token value types
33
- * such as {@link DesignToken.Border}
207
+ * The {@link (Library:namespace).Recipe | recipes} of the functions of `F`,
208
+ * by the same keys. Made by {@link recipes}.
34
209
  *
35
210
  * @public
36
211
  */
37
- type DeepAlias<V extends DesignToken.Values.Any, T extends Context<any>> = {
38
- [K in keyof V]: V[K] extends DesignToken.Values.Any ? V[K] | Alias<DesignToken.TokenByValue<V[K]>, T> | DeepAlias<V[K], T> : never;
212
+ export type Recipes<F extends Record<string, (...args: any[]) => unknown>> = {
213
+ readonly [K in keyof F]: Recipe<Parameters<F[K]>, ReturnType<F[K]>>;
39
214
  };
40
215
  /**
41
- * Context object provided to {@link (Library:namespace).Alias} values at runtime
216
+ * A function that replaces a recipe's in a library made by `extend()`.
217
+ * Made by {@link override}.
218
+ *
219
+ * @public
220
+ */
221
+ export interface RecipeOverride {
222
+ readonly recipe: Recipe<any, unknown>;
223
+ readonly fn: (...args: any[]) => unknown;
224
+ }
225
+ /**
226
+ * Options of `extend()`.
227
+ *
228
+ * @public
229
+ */
230
+ export interface ExtendOptions {
231
+ /**
232
+ * Recipes whose function is replaced in the new library, for every
233
+ * token that computes its value with them, including the tokens it
234
+ * doesn't restate.
235
+ */
236
+ readonly recipes?: readonly RecipeOverride[];
237
+ }
238
+ /**
239
+ * A {@link (Library:namespace).Ref} to each token of a library of shape
240
+ * `R`, at the path of the token in `T`, the shape of a group of it. Made by
241
+ * {@link refs}.
42
242
  *
43
243
  * @public
44
244
  */
45
- type Context<T extends {}, R extends {} = T> = {
46
- [K in keyof Readonly<T>]: T[K] extends DesignToken.Any ? Readonly<T[K]> : K extends "type" ? DesignToken.Type : T[K] extends {} ? Context<T[K], R> : never;
245
+ export type Refs<T extends {}, R extends {} = T> = {
246
+ readonly [K in keyof T as K extends keyof DesignToken.Group ? never : K]-?: T[K] extends DesignToken.Shape ? Ref<DesignToken.ValueByToken<T[K]>, TokenValues<R>> : T[K] extends {} ? Refs<T[K], R> : never;
47
247
  };
48
248
  /**
49
249
  * A token in a {@link (Library:namespace).TokenLibrary}
50
250
  *
251
+ * @remarks
252
+ * `G` is the type inherited from ancestor groups, used when
253
+ * `T` does not declare a `$type`.
254
+ *
51
255
  * @public
52
256
  */
53
- type Token<T extends DesignToken.Any, C extends {}> = {
54
- set(value: DesignToken.ValueByToken<T> | Alias<T, C>): void;
257
+ export type Token<T extends DesignToken.Shape, C extends {}, G extends string = never> = {
258
+ set(value: ConfigTokenValue<T, C> | DesignToken.ReadonlyValueByToken<T>): void;
259
+ /**
260
+ * A JSON representation of the token's name, type, resolved value,
261
+ * description, extensions and deprecation.
262
+ */
55
263
  toString(): string;
56
- readonly type: DesignToken.TypeByToken<T>;
57
- readonly extensions: Record<string, any>;
58
- readonly value: DesignToken.ValueByToken<T>;
59
- readonly description: string;
264
+ readonly $type: DesignToken.TypeByToken<T, G>;
265
+ readonly $extensions: Record<string, any>;
266
+ readonly $value: DesignToken.ReadonlyValueByToken<T>;
267
+ readonly $description: string;
268
+ /**
269
+ * `false`, `true`, or the explanation the token was deprecated with.
270
+ */
271
+ readonly $deprecated: boolean | string;
60
272
  readonly name: string;
61
273
  };
274
+ type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : never;
275
+ /**
276
+ * The type a group passes on to its descendant tokens, following
277
+ * {@link https://www.designtokens.org/tr/2025.10/format/#type-1 | DTCG group type inheritance}.
278
+ *
279
+ * @remarks
280
+ * A group passes on its own `$type` only when the group's shape declares a
281
+ * required, single, literal `$type`. A group with an optional or non-literal
282
+ * `$type` may set any type at runtime, so it passes on nothing. A group
283
+ * without a `$type` passes on its ancestor's type, `G`.
284
+ *
285
+ * @public
286
+ */
287
+ export type GroupType<T, G extends string = never> = "$type" extends keyof T ? {} extends Pick<T, "$type"> ? never : T["$type"] extends string ? string extends T["$type"] ? never : true extends IsUnion<T["$type"]> ? never : T["$type"] : never : G;
288
+ /**
289
+ * The value of a token in a configuration object, in a library of shape
290
+ * `R`: a {@link (Library:namespace).DeepValue} of the token's value type.
291
+ *
292
+ * @public
293
+ */
294
+ export type ConfigTokenValue<T extends DesignToken.Shape, R extends {}> = DeepValue<DesignToken.ValueByToken<T>, TokenValues<R>>;
295
+ /**
296
+ * A token in a configuration object.
297
+ *
298
+ * @remarks
299
+ * The token's `$type` may only be omitted when it matches the type
300
+ * inherited from ancestor groups, `G`.
301
+ *
302
+ * @public
303
+ */
304
+ export type ConfigValue<T extends DesignToken.Shape, R extends {}, G extends string = never> = T | (Omit<T, "$type" | "$value"> & ([G] extends [never] ? {
305
+ $type: DesignToken.TypeByToken<T>;
306
+ } : [DesignToken.TypeByToken<T, G>] extends [G] ? {
307
+ $type?: DesignToken.TypeByToken<T, G>;
308
+ } : {
309
+ $type: DesignToken.TypeByToken<T>;
310
+ }) & {
311
+ $value: ConfigTokenValue<T, R>;
312
+ });
62
313
  /**
63
314
  * A configuration object provided to {@link Library.create}
64
315
  *
316
+ * @remarks
317
+ * `G` is the type inherited from ancestor groups. `S` is the shape of a
318
+ * source library being extended: its group types are inherited, and its
319
+ * tokens may be overridden without restating their type.
320
+ *
65
321
  * @public
66
322
  */
67
- type Config<T extends {}, R extends {} = T> = {
68
- [K in keyof T]: T[K] extends DesignToken.Any ? ConfigValue<T[K], R> : T[K] extends {} ? Config<T[K], R> : never;
323
+ export type Config<T extends {}, R extends {} = T, G extends string = never, S = {}> = ConfigGroup<T, R, GroupType<T, GroupType<S, G>>, S>;
324
+ type ConfigGroup<T extends {}, R extends {}, G extends string, S> = {
325
+ [K in keyof T]: K extends keyof S ? S[K] extends DesignToken.Shape ? unknown : ConfigEntry<T, R, G, S, K> : ConfigEntry<T, R, G, S, K>;
69
326
  };
327
+ type ConfigEntry<T extends {}, R extends {}, G extends string, S, K extends keyof T> = T[K] extends DesignToken.Shape ? ConfigValue<T[K], R, G> : K extends keyof DesignToken.Group ? T[K] : T[K] extends {} ? Config<T[K], R, G, K extends keyof S ? S[K] : {}> : T[K] extends null | undefined ? T[K] : never;
70
328
  /**
329
+ * Checks the `$value` of each token in a config of shape `T` against
330
+ * its `$type`. It's needed when `T` is inferred from the config, because
331
+ * the inferred `$value` is the config's own value, so
332
+ * {@link (Library:namespace).Config} alone can't reject it.
333
+ *
334
+ * @remarks
335
+ * Only the values of tokens that declare a {@link DesignToken.Type} are
336
+ * checked. A token can't have a child (a key without a `$` prefix).
337
+ *
338
+ * Tokens are found by their keys rather than by `T[K] extends Shape`,
339
+ * which TypeScript doesn't resolve for an inferred `T` with extra keys.
340
+ * An `any` config, such as `Library.create<any>()`, isn't checked.
341
+ *
71
342
  * @public
72
343
  */
73
- type ConfigValue<T extends DesignToken.Any, R extends {}> = T | (Omit<T, "value"> & {
74
- value: Library.Alias<T, Context<R>> | Library.DeepAlias<DesignToken.ValueByToken<T>, Context<R>>;
75
- });
344
+ export type ConfigValueCheck<T, R extends {}> = 0 extends 1 & T ? unknown : {
345
+ [K in keyof T]: 0 extends 1 & T[K] ? unknown : "$value" extends keyof T[K] ? ("$type" extends keyof T[K] ? TokenValueCheck<T[K][Extract<"$type", keyof T[K]>], R> : unknown) & {
346
+ [P in Exclude<keyof T[K], `$${string}`>]?: never;
347
+ } : T[K] extends object ? ConfigValueCheck<T[K], R> : unknown;
348
+ };
349
+ type TokenValueCheck<Type, R extends {}> = [Type] extends [
350
+ keyof DesignToken.ValueByType
351
+ ] ? {
352
+ $value: ConfigTokenValue<{
353
+ $type: Type;
354
+ $value: DesignToken.ValueByType[Type];
355
+ }, R>;
356
+ } : unknown;
357
+ /**
358
+ * Gives each `$type` in a config literal a contextual type, so that
359
+ * TypeScript keeps an inferred `$type` literal, such as
360
+ * `DesignToken.Type.Color`, instead of widening it to `DesignToken.Type`.
361
+ * {@link (Library:namespace).ConfigValueCheck} needs the literal.
362
+ *
363
+ * @remarks
364
+ * Configs are typed `ConfigTypeHint | {}`, so a config without an index
365
+ * signature, such as a typed variable, is still accepted.
366
+ *
367
+ * @public
368
+ */
369
+ export type ConfigTypeHint = {
370
+ readonly [key: string]: ConfigTypeHint | DesignToken.Type | (string & {}) | number | boolean | object | null | undefined;
371
+ };
372
+ /**
373
+ * A configuration object provided to {@link (Library:namespace).Library.extend}.
374
+ * All tokens and groups of the source library are optional, and token
375
+ * values may be static values, refs or computed values. Token types are inherited
376
+ * from the source library.
377
+ *
378
+ * @public
379
+ */
380
+ export type ExtendConfig<T extends {}, R extends {} = T> = {
381
+ [K in keyof T]?: T[K] extends DesignToken.Shape ? Omit<T[K], "$type" | "$value"> & {
382
+ $type?: DesignToken.TypeByToken<T[K]>;
383
+ $value: ConfigTokenValue<T[K], R>;
384
+ } : K extends keyof DesignToken.Group ? T[K] : T[K] extends {} ? ExtendConfig<T[K], R> : never;
385
+ };
76
386
  /**
77
387
  * @public
78
388
  */
79
- const create: <T extends {} = any>(config: Library.Config<T, T>) => Library.Library<T>;
389
+ export const create: <T extends NoRoot<T> = any>(config: Library.Config<T, T> & NoInfer<ConfigValueCheck<T, T>> & (ConfigTypeHint | {})) => Library.Library<T>;
390
+ export {};
391
+ }
392
+ /**
393
+ * Thrown when reading a token whose value refers back to itself through
394
+ * one or more refs or computed values.
395
+ *
396
+ * @public
397
+ */
398
+ export declare class CircularReferenceError extends Error {
399
+ readonly name = "CircularReferenceError";
400
+ /**
401
+ * The names of the tokens in the cycle, starting and ending with the
402
+ * same token.
403
+ */
404
+ readonly chain: string[];
405
+ constructor(chain: string[]);
80
406
  }
407
+ export {};