design-token-library 0.1.0 → 0.3.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,6 +1,8 @@
1
+ import { COMPUTED, RECIPE, isComputed } from "./recipe.js";
2
+ import { REF, isRef } from "./ref.js";
1
3
  import { getNotifier } from "./notifier.js";
2
4
  import { Queue } from "./queue.js";
3
- import { empty } from "./utilities.js";
5
+ import { empty, isToken } from "./utilities.js";
4
6
  import { Watcher } from "./watcher.js";
5
7
  /**
6
8
  * @public
@@ -11,168 +13,474 @@ export var Library;
11
13
  * @public
12
14
  */
13
15
  Library.create = (config) => {
16
+ // The runtime library is untyped; its shape is guaranteed by `config`.
14
17
  return LibraryImpl.create(config);
15
18
  };
16
19
  })(Library || (Library = {}));
20
+ /**
21
+ * Thrown when reading a token whose value refers back to itself through
22
+ * one or more refs or computed values.
23
+ *
24
+ * @public
25
+ */
26
+ export class CircularReferenceError extends Error {
27
+ name = "CircularReferenceError";
28
+ /**
29
+ * The names of the tokens in the cycle, starting and ending with the
30
+ * same token.
31
+ */
32
+ chain;
33
+ constructor(chain) {
34
+ super(`Circular reference: ${chain.join(" → ")}`);
35
+ this.chain = chain;
36
+ }
37
+ }
38
+ /**
39
+ * The changes to the configured values in a library's tokens: its own, and
40
+ * those of the libraries it extends, whose values it takes until its tokens
41
+ * are set.
42
+ */
43
+ const configurationVersionOf = (scope) => {
44
+ let version = 0;
45
+ for (let s = scope; s; s = s.source) {
46
+ version += s.configurationVersion;
47
+ }
48
+ return version;
49
+ };
50
+ /**
51
+ * For a library's tokens, the tokens whose configured values refer to each,
52
+ * and the configuration it was made from.
53
+ */
54
+ const dependentsIndexes = new WeakMap();
55
+ /**
56
+ * The tokens currently resolving their values, outermost first.
57
+ */
58
+ const resolving = [];
17
59
  const isObject = (value) => {
18
60
  return typeof value === "object" && value !== null;
19
61
  };
62
+ const isGroup = (value) => {
63
+ return isObject(value) && !Array.isArray(value) && !isToken(value);
64
+ };
65
+ const GROUP_METADATA = [
66
+ "$type",
67
+ "$description",
68
+ "$extensions",
69
+ "$deprecated",
70
+ ];
20
71
  /**
21
- * @internal
72
+ * Names beginning with `$` are for format properties, except `$root`: the
73
+ * reserved name of a group's own token.
22
74
  */
23
- const isToken = (value) => {
24
- return isObject(value) && "value" in value;
75
+ const isChildKey = (key) => {
76
+ return !key.startsWith("$") || key === "$root";
25
77
  };
26
- const isGroup = (value) => {
27
- return isObject(value) && !isToken(value);
78
+ /**
79
+ * Records a group's own `$`-prefixed properties on it. They are not
80
+ * enumerable, so they never appear as children when a group is walked, and
81
+ * they let extending libraries resolve inherited types.
82
+ */
83
+ const defineGroupMetadata = (group, ...sources) => {
84
+ for (const property of GROUP_METADATA) {
85
+ for (const source of sources) {
86
+ if (source[property] !== undefined) {
87
+ Reflect.defineProperty(group, property, {
88
+ value: source[property],
89
+ });
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ };
95
+ const defineToken = (library, key, get) => {
96
+ Reflect.defineProperty(library, key, {
97
+ get() {
98
+ const token = get();
99
+ // Token access is tracked because a ref reads its token from
100
+ // the library
101
+ Watcher.track(token);
102
+ return token;
103
+ },
104
+ enumerable: true,
105
+ });
28
106
  };
29
- const isAlias = (value) => {
30
- return typeof value === "function";
107
+ const createToken = (key, name, config, scope, typeContext, queue) => {
108
+ for (const property of Object.keys(config)) {
109
+ if (!property.startsWith("$")) {
110
+ throw new Error(`"${name}" has a $value, so it is a token and cannot have a child "${property}".`);
111
+ }
112
+ }
113
+ const { $value, $type, $description, $extensions, $deprecated } = config;
114
+ const resolvedType = $type || typeContext;
115
+ if (!resolvedType) {
116
+ throw new Error(`No '$type' found for token '${key}'. Types cannot be inferred, please add a $type to the token or to a group ancestor.`);
117
+ }
118
+ return new LibraryToken(name, $value, resolvedType, scope, $description || "", $extensions || {}, $deprecated ?? false, queue);
31
119
  };
32
- const recurseCreate = (name, library, config, context, typeContext, queue) => {
120
+ const recurseCreate = (name, library, config, scope, typeContext, queue) => {
121
+ defineGroupMetadata(library, config);
122
+ typeContext = config.$type || typeContext;
33
123
  for (const key in config) {
34
- if (key === "type") {
35
- typeContext = config[key];
124
+ if (!isChildKey(key)) {
36
125
  continue;
37
126
  }
38
127
  const _name = name.length === 0 ? key : `${name}.${key}`;
39
128
  if (isGroup(config[key])) {
129
+ const group = {};
40
130
  Reflect.defineProperty(library, key, {
41
- value: {},
131
+ value: group,
42
132
  enumerable: true,
43
133
  });
44
- recurseCreate(_name, library[key], config[key], context, config[key].type || typeContext, queue);
45
- Object.freeze(library[key]);
134
+ recurseCreate(_name, group, config[key], scope, typeContext, queue);
135
+ Object.freeze(group);
46
136
  }
47
137
  else if (isToken(config[key])) {
48
- const { value, type, description, extensions } = config[key];
49
- if (!type && !typeContext) {
50
- throw new Error(`No 'type' found for token '${key}'. Types cannot be inferred, please add a type to the token or to a group ancestor.`);
51
- }
52
- const token = new LibraryToken(_name, value, type || typeContext, context, description || "", extensions || {}, queue);
53
- Reflect.defineProperty(library, key, {
54
- get() {
55
- // Token access needs to be tracked because an alias token
56
- // is a function that returns a token
57
- Watcher.track(token);
58
- return token;
59
- },
60
- enumerable: true,
61
- });
138
+ const token = createToken(key, _name, config[key], scope, typeContext, queue);
139
+ defineToken(library, key, () => token);
62
140
  }
63
141
  }
64
142
  };
65
- const recurseExtend = (name, sourceTokens, extendedTokens, config, // TODO allow new config options
66
- context, typeContext, queue) => {
67
- const keys = new Set(Object.keys(sourceTokens).concat(Object.keys(config))); // Remove duplicate keys
143
+ const recurseExtend = (name, sourceTokens, extendedTokens, config, scope, typeContext, queue, inherited) => {
144
+ // An extension can restate a group's type, or give one to a group that
145
+ // has none, but can't change it, as for a token.
146
+ const sourceType = Reflect.get(sourceTokens, "$type");
147
+ if (sourceType &&
148
+ config.$type !== undefined &&
149
+ config.$type !== sourceType) {
150
+ throw new Error(`An override cannot change the '$type' of group '${name}' from '${sourceType}' to '${config.$type}'.`);
151
+ }
152
+ const groupType = sourceType || config.$type;
153
+ defineGroupMetadata(extendedTokens, { $type: groupType },
154
+ // An extension may restate the group's other metadata.
155
+ { ...config, $type: undefined }, sourceTokens);
156
+ typeContext = groupType || typeContext;
157
+ const keys = new Set(Object.keys(sourceTokens)
158
+ .concat(Object.keys(config))
159
+ .filter(isChildKey)); // Remove duplicate keys
68
160
  for (const key of keys) {
69
- const sourceHasKey = key in sourceTokens;
70
- const configHasKey = key in config;
71
161
  const _name = name.length === 0 ? key : `${name}.${key}`;
72
- const keyIsGroup = isGroup(sourceTokens[key]) || isGroup(config[key]);
73
- const keyIsToken = isToken(sourceTokens[key]) || isToken(config[key]);
74
- if (key === "type") {
75
- typeContext = sourceTokens[key];
76
- continue;
162
+ const source = Reflect.getOwnPropertyDescriptor(sourceTokens, key);
163
+ const configValue = config[key];
164
+ // A group of the source is a data property and a token an accessor,
165
+ // so telling them apart doesn't make the token of an extension.
166
+ if (source ? "value" in source : isGroup(configValue)) {
167
+ const group = {};
168
+ Reflect.defineProperty(extendedTokens, key, {
169
+ value: group,
170
+ enumerable: true,
171
+ });
172
+ if (source) {
173
+ recurseExtend(_name, source.value, group, configValue || {}, scope, typeContext, queue, inherited);
174
+ }
175
+ else {
176
+ recurseCreate(_name, group, configValue, scope, typeContext, queue);
177
+ }
178
+ Object.freeze(group);
77
179
  }
78
- if (keyIsGroup) {
79
- Reflect.defineProperty(extendedTokens, key, Object.create(sourceTokens[key]));
80
- if (sourceHasKey) {
81
- recurseExtend(_name, sourceTokens[key], extendedTokens[key], config[key] || {}, context, (sourceTokens.type || typeContext), queue);
180
+ else if (source) {
181
+ const token = new InheritedToken(() => untracked(() => sourceTokens[key]), scope, queue, configValue);
182
+ // A token the extension configures is made now, so its config
183
+ // is checked when the library is.
184
+ if (configValue !== undefined) {
185
+ token.get();
82
186
  }
83
- else if (configHasKey) {
84
- // This will always be the case
85
- recurseCreate(_name, sourceTokens[key], extendedTokens[key], context, (sourceTokens.type || typeContext), queue);
187
+ if (configValue === undefined || !("$value" in configValue)) {
188
+ inherited.push(token);
86
189
  }
190
+ defineToken(extendedTokens, key, () => token.get());
87
191
  }
88
- else if (keyIsToken) {
89
- const token = sourceTokens[key] !== undefined
90
- ? extendToken(sourceTokens[key], context, queue, config[key])
91
- : new LibraryToken(_name, config[key].value, config[key].type || typeContext, context, config[key].description || "", config[key].extensions || {}, queue);
92
- Reflect.defineProperty(extendedTokens, key, {
93
- get() {
94
- // Token access needs to be tracked because an alias token
95
- // is a function that returns a token
96
- Watcher.track(token);
97
- return token;
98
- },
99
- enumerable: true,
100
- });
192
+ else if (isToken(configValue)) {
193
+ const token = createToken(key, _name, configValue, scope, typeContext, queue);
194
+ defineToken(extendedTokens, key, () => token);
101
195
  }
102
196
  }
103
197
  };
104
- function extendToken(token, context, queue, value) {
198
+ /**
199
+ * Reads without the read being tracked by the token that is resolving.
200
+ */
201
+ const untracked = (read) => {
202
+ const stopWatching = Watcher.use({ watch() { } });
203
+ try {
204
+ return read();
205
+ }
206
+ finally {
207
+ stopWatching();
208
+ }
209
+ };
210
+ /**
211
+ * Unsubscribes the link of a collected {@link InheritedToken} from its
212
+ * source token.
213
+ */
214
+ const links = new FinalizationRegistry((unlink) => unlink());
215
+ /**
216
+ * The token of an extension for a token of the library it extends. It is
217
+ * made when it is first read, so an extension costs what it reads (#113).
218
+ *
219
+ * Until it is set, it has the value of its source token and follows its
220
+ * changes, through a link on the source token that holds it weakly: the
221
+ * library it extends doesn't keep an extension alive that nothing else
222
+ * refers to. It follows once it is made, or once its extension has a
223
+ * subscriber, which is told of a change to a token it hasn't read.
224
+ */
225
+ class InheritedToken {
226
+ source;
227
+ scope;
228
+ queue;
229
+ node;
230
+ token = null;
231
+ unlink = null;
232
+ detached;
233
+ constructor(source, scope, queue, node) {
234
+ this.source = source;
235
+ this.scope = scope;
236
+ this.queue = queue;
237
+ this.node = node;
238
+ // An override has a value of its own.
239
+ this.detached = node !== undefined && "$value" in node;
240
+ }
241
+ get() {
242
+ if (this.token !== null) {
243
+ return this.token;
244
+ }
245
+ const token = extendToken(this.source(), this.scope, this.queue, this.node, this);
246
+ this.token = token;
247
+ this.follow();
248
+ return token;
249
+ }
250
+ follow() {
251
+ if (this.unlink !== null || this.detached) {
252
+ return;
253
+ }
254
+ // The link holds only a weak reference, so that the registry can
255
+ // collect this token, and nothing that refers to it.
256
+ const target = new WeakRef(this);
257
+ const notifier = getNotifier(this.source());
258
+ const link = {
259
+ onChange() {
260
+ target.deref()?.get().onChange();
261
+ },
262
+ };
263
+ const unlink = () => notifier.unsubscribe(link);
264
+ notifier.subscribe(link);
265
+ links.register(this, unlink, link);
266
+ this.unlink = () => {
267
+ unlink();
268
+ links.unregister(link);
269
+ };
270
+ }
271
+ /**
272
+ * Stops following the source token, when the token is set.
273
+ */
274
+ detach() {
275
+ this.unlink?.();
276
+ this.unlink = null;
277
+ this.detached = true;
278
+ }
279
+ }
280
+ function extendToken(token, scope, queue, node, inherited) {
281
+ if (node?.$type !== undefined && node.$type !== token.$type) {
282
+ throw new Error(`An override cannot change the '$type' of token '${token.name}' from '${token.$type}' to '${node.$type}'.`);
283
+ }
105
284
  const extendingToken = Object.create(token);
106
- extendingToken.context = context;
285
+ extendingToken.scope = scope;
107
286
  extendingToken.cached = empty;
287
+ extendingToken.resolving = false;
288
+ extendingToken.notifying = false;
289
+ // Its own dependencies; sharing the source's set would let either token
290
+ // drop the other's subscriptions.
291
+ extendingToken.subscriptions = new Set();
292
+ extendingToken.previousSubscriptions = null;
108
293
  extendingToken.watchContext = extendingToken;
109
294
  extendingToken.queue = queue;
110
- if (value !== undefined) {
111
- extendingToken.raw = value;
295
+ // An extension can restate the token's metadata along with its value.
296
+ if (node?.$description !== undefined) {
297
+ extendingToken._description = node.$description;
298
+ }
299
+ // A new object, so mutating it can't leak to the source or the override.
300
+ extendingToken._extensions = { ...token.$extensions, ...node?.$extensions };
301
+ if (node?.$deprecated !== undefined) {
302
+ extendingToken._deprecated = node.$deprecated;
303
+ }
304
+ if (node !== undefined && "$value" in node) {
305
+ extendingToken.raw = node.$value;
112
306
  }
113
307
  else {
114
- // Subscribe to changes
115
- // spy on set, unsubscribe when set
116
- const subscriber = {
117
- onChange() {
118
- extendingToken.onChange();
119
- },
120
- };
121
- // token.value;
122
- getNotifier(token).subscribe(subscriber);
123
- const set = extendingToken.set;
308
+ // Not extendingToken.set: when the extended token is an extension
309
+ // that hasn't been set, that is the extended token's own wrapper,
310
+ // which sets the extended token.
311
+ const set = LibraryToken.prototype.set;
124
312
  extendingToken.set = (value) => {
125
- getNotifier(token).unsubscribe(subscriber);
313
+ inherited.detach();
314
+ // Own the inherited value, so set() detaches even when the
315
+ // new value is identical and doesn't notify.
316
+ extendingToken.raw = extendingToken.raw;
126
317
  set.call(extendingToken, value);
127
318
  extendingToken.set = set;
128
319
  };
129
320
  }
130
321
  return extendingToken;
131
322
  }
132
- const recurseResolve = (value, context) => {
133
- const r = Array.isArray(value) ? [] : {};
134
- for (const key in value) {
135
- let v = value[key];
136
- if (isAlias(v)) {
137
- v = v(context);
323
+ /**
324
+ * The number of links in an object's prototype chain.
325
+ */
326
+ const prototypeDepth = (value) => {
327
+ let depth = 0;
328
+ for (let prototype = Object.getPrototypeOf(value); prototype !== null; prototype = Object.getPrototypeOf(prototype)) {
329
+ depth++;
330
+ }
331
+ return depth;
332
+ };
333
+ /**
334
+ * Tests whether an object is plain data, which may hold refs: an array that
335
+ * isn't a subclass of `Array`, or an object whose prototype chain has at most
336
+ * one link, such as an object literal from any realm or `Object.create(null)`.
337
+ * Any other object, such as a class instance, a `Date` or a `Map`, is a value
338
+ * as it is.
339
+ */
340
+ const isPlain = (value) => {
341
+ return Array.isArray(value)
342
+ ? prototypeDepth(value) === 2
343
+ : prototypeDepth(value) <= 1;
344
+ };
345
+ /**
346
+ * The token at a path of a library. Each group and the token are read from
347
+ * the library, so the token that reads it depends on it.
348
+ */
349
+ const tokenAt = (tokens, path) => {
350
+ let node = tokens;
351
+ for (const key of path) {
352
+ node = isObject(node) ? Reflect.get(node, key) : undefined;
353
+ }
354
+ if (!(node instanceof LibraryToken)) {
355
+ throw new Error(`The library has no token "${path.join(".")}".`);
356
+ }
357
+ return node;
358
+ };
359
+ /**
360
+ * Resolves a value in a library's scope. A ref is replaced by its token's
361
+ * value, as it is: the value is resolved and frozen already, so it isn't
362
+ * copied again. A computed value is replaced by the result of its recipe's
363
+ * function, or of the function that overrides it in the scope, called with
364
+ * its arguments resolved. Plain data is copied from its own properties, including
365
+ * symbol-keyed and non-enumerable ones, with its refs resolved, and the copy
366
+ * is frozen, so the config isn't. A getter is read once, into a data
367
+ * property. An object that is in the value twice, or in itself, has one copy.
368
+ * Anything else, such as a class instance or a function, is returned as it
369
+ * is: it can't be rebuilt, so it isn't copied, searched or frozen.
370
+ */
371
+ const resolve = (value, scope) => {
372
+ const copies = new Map();
373
+ const walk = (value) => {
374
+ if (!isObject(value)) {
375
+ return value;
138
376
  }
139
- if (isToken(v)) {
140
- v = v.value;
377
+ if (isRef(value)) {
378
+ return tokenAt(scope.tokens, value[REF]).$value;
141
379
  }
142
- if (isObject(v)) {
143
- r[key] = recurseResolve(v, context);
380
+ if (isComputed(value)) {
381
+ const { recipe, args } = value[COMPUTED];
382
+ const fn = scope.recipes.get(recipe) ?? recipe[RECIPE].fn;
383
+ return walk(fn(...args.map(walk)));
144
384
  }
145
- else {
146
- r[key] = v;
385
+ if (copies.has(value)) {
386
+ return copies.get(value);
147
387
  }
388
+ if (!isPlain(value)) {
389
+ return value;
390
+ }
391
+ if (Array.isArray(value)) {
392
+ const copy = new Array(value.length);
393
+ copies.set(value, copy);
394
+ for (let i = 0; i < value.length; i++) {
395
+ copy[i] = walk(value[i]);
396
+ }
397
+ return Object.freeze(copy);
398
+ }
399
+ const copy = Object.create(Object.getPrototypeOf(value));
400
+ copies.set(value, copy);
401
+ for (const key of Reflect.ownKeys(value)) {
402
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
403
+ Reflect.defineProperty(copy, key, "value" in descriptor
404
+ ? { ...descriptor, value: walk(descriptor.value) }
405
+ : {
406
+ value: walk(Reflect.get(value, key)),
407
+ enumerable: descriptor.enumerable,
408
+ writable: true,
409
+ configurable: true,
410
+ });
411
+ }
412
+ return Object.freeze(copy);
413
+ };
414
+ return walk(value);
415
+ };
416
+ const assertNoRoot = (config) => {
417
+ if ("$root" in config) {
418
+ throw new Error(`"$root" is the token of a group, so it cannot be at the top level of a library.`);
148
419
  }
149
- return r;
150
420
  };
421
+ /**
422
+ * The runtime library. It is untyped internally; {@link Library.create}
423
+ * exposes it through the typed {@link (Library:namespace).Library} interface.
424
+ */
151
425
  class LibraryImpl {
152
- tokens;
426
+ scope;
153
427
  queue;
154
- constructor(tokens, queue) {
155
- this.tokens = tokens;
428
+ tokens;
429
+ /**
430
+ * The inherited tokens of an extension. Each follows its source token
431
+ * once it is read, and all of them once the library has a subscriber,
432
+ * after which this is null.
433
+ */
434
+ inherited;
435
+ constructor(scope, queue, inherited = []) {
436
+ this.scope = scope;
156
437
  this.queue = queue;
438
+ this.tokens = scope.tokens;
439
+ this.inherited = inherited;
440
+ // Groups are frozen as they are built; the root is frozen here.
441
+ Object.freeze(this.tokens);
157
442
  }
158
443
  subscribe(subscriber) {
444
+ // A subscriber is told of a change to a token of the source that the
445
+ // extension hasn't read.
446
+ if (this.inherited !== null) {
447
+ for (const token of this.inherited) {
448
+ token.follow();
449
+ }
450
+ this.inherited = null;
451
+ }
159
452
  this.queue.subscribe(subscriber);
160
453
  }
161
454
  unsubscribe(subscriber) {
162
455
  this.queue.unsubscribe(subscriber);
163
456
  }
164
- extend(config) {
165
- // TODO should not type Library.Config<any>
457
+ extend(config, options = {}) {
458
+ assertNoRoot(config);
166
459
  const queue = new Queue();
167
- const tokens = {};
168
- recurseExtend("", this.tokens, tokens, config, tokens, null, queue);
169
- return new LibraryImpl(tokens, queue);
460
+ const recipes = new Map(this.scope.recipes);
461
+ for (const { recipe, fn } of options.recipes ?? []) {
462
+ recipes.set(recipe, fn);
463
+ }
464
+ const scope = {
465
+ tokens: {},
466
+ recipes,
467
+ configurationVersion: 0,
468
+ source: this.scope,
469
+ };
470
+ const inherited = [];
471
+ recurseExtend("", this.tokens, scope.tokens, config, scope, null, queue, inherited);
472
+ return new LibraryImpl(scope, queue, inherited);
170
473
  }
171
474
  static create(config) {
475
+ assertNoRoot(config);
172
476
  const queue = new Queue();
173
- const tokens = {};
174
- recurseCreate("", tokens, config, tokens, null, queue);
175
- return new LibraryImpl(tokens, queue);
477
+ const scope = {
478
+ tokens: {},
479
+ recipes: new Map(),
480
+ configurationVersion: 0,
481
+ };
482
+ recurseCreate("", scope.tokens, config, scope, null, queue);
483
+ return new LibraryImpl(scope, queue);
176
484
  }
177
485
  }
178
486
  /**
@@ -181,72 +489,334 @@ class LibraryImpl {
181
489
  class LibraryToken {
182
490
  name;
183
491
  _type;
184
- context;
492
+ scope;
185
493
  _description;
186
- _extensions;
494
+ _deprecated;
187
495
  queue;
188
496
  raw;
189
497
  cached = empty;
498
+ resolving = false;
499
+ notifying = false;
190
500
  subscriptions = new Set();
191
- constructor(name, value, _type, context, _description, _extensions, queue) {
501
+ /**
502
+ * While resolving, the subscriptions from the previous resolution. Sources
503
+ * watched again keep their subscription instead of re-subscribing.
504
+ */
505
+ previousSubscriptions = null;
506
+ _extensions;
507
+ constructor(name, value, _type, scope, _description, extensions, _deprecated, queue) {
192
508
  this.name = name;
193
509
  this._type = _type;
194
- this.context = context;
510
+ this.scope = scope;
195
511
  this._description = _description;
196
- this._extensions = _extensions;
512
+ this._deprecated = _deprecated;
197
513
  this.queue = queue;
198
514
  this.raw = value;
199
- this.context = context;
515
+ // A copy, so changing the config's top-level keys can't change the token.
516
+ this._extensions = { ...extensions };
517
+ }
518
+ get $deprecated() {
519
+ return this._deprecated;
200
520
  }
201
- get type() {
521
+ get $type() {
202
522
  return this._type;
203
523
  }
204
- get description() {
524
+ get $description() {
205
525
  return this._description;
206
526
  }
207
- get extensions() {
527
+ get $extensions() {
208
528
  return this._extensions;
209
529
  }
210
530
  /**
211
531
  * Gets the token value
212
532
  */
213
- get value() {
533
+ get $value() {
214
534
  if (this.cached !== empty) {
215
535
  return this.cached;
216
536
  }
217
- this.disconnect();
537
+ if (this.resolving) {
538
+ const chain = resolving
539
+ .slice(resolving.indexOf(this))
540
+ .map((token) => token.name);
541
+ throw new CircularReferenceError([...chain, this.name]);
542
+ }
543
+ const previous = this.subscriptions;
544
+ this.previousSubscriptions = previous;
545
+ this.subscriptions = new Set();
546
+ this.resolving = true;
547
+ resolving.push(this);
218
548
  const stopWatching = Watcher.use(this);
219
- const raw = isAlias(this.raw) ? this.raw(this.context) : this.raw;
220
- const normalized = isToken(raw) ? raw.value : raw;
221
- const value = isObject(normalized)
222
- ? recurseResolve(normalized, this.context)
223
- : normalized;
224
- this.cached = value;
225
- stopWatching();
226
- return value;
549
+ try {
550
+ // The config's types guarantee what the raw value resolves to.
551
+ const value = resolve(this.raw, this.scope);
552
+ this.cached = value;
553
+ return value;
554
+ }
555
+ finally {
556
+ this.resolving = false;
557
+ resolving.pop();
558
+ stopWatching();
559
+ this.previousSubscriptions = null;
560
+ // Unsubscribe from sources that were not watched this time.
561
+ for (const notifier of previous) {
562
+ if (!this.subscriptions.has(notifier)) {
563
+ notifier.unsubscribe(this);
564
+ }
565
+ }
566
+ }
567
+ }
568
+ toString() {
569
+ return JSON.stringify({
570
+ name: this.name,
571
+ $type: this.$type,
572
+ $value: this.$value,
573
+ $description: this.$description,
574
+ $extensions: this.$extensions,
575
+ $deprecated: this.$deprecated,
576
+ });
227
577
  }
228
578
  set(value) {
579
+ if (Object.is(this.raw, value)) {
580
+ return;
581
+ }
229
582
  this.raw = value;
583
+ this.scope.configurationVersion++;
230
584
  this.onChange();
231
585
  }
232
586
  onChange() {
233
- this.queue.add(this);
234
- this.cached = empty;
235
- getNotifier(this).notify();
587
+ // A circular reference that threw while resolving still leaves the
588
+ // tokens subscribed to each other, so don't notify re-entrantly.
589
+ if (this.notifying) {
590
+ return;
591
+ }
592
+ this.notifying = true;
593
+ try {
594
+ this.queue.add(this);
595
+ this.cached = empty;
596
+ getNotifier(this).notify();
597
+ }
598
+ finally {
599
+ this.notifying = false;
600
+ }
236
601
  }
237
602
  watch(source) {
238
603
  const notifier = getNotifier(source);
239
- notifier.subscribe(this);
604
+ if (this.subscriptions.has(notifier)) {
605
+ return;
606
+ }
240
607
  this.subscriptions.add(notifier);
608
+ if (!this.previousSubscriptions?.has(notifier)) {
609
+ notifier.subscribe(this);
610
+ }
241
611
  }
242
612
  /**
243
- * Disconnect the token from it's subscriptions
613
+ * The value a token was configured or last set with. An extension's
614
+ * token that has no value of its own inherits its source's.
244
615
  */
245
- disconnect() {
246
- for (const record of this.subscriptions.values()) {
247
- record.unsubscribe(this);
248
- this.subscriptions.delete(record);
616
+ static configuredValue(token) {
617
+ return token.raw;
618
+ }
619
+ /**
620
+ * The tokens that the refs in a token's configured value are to, in the
621
+ * token's library, searched as {@link resolve} resolves them: in plain
622
+ * data and in the arguments of computed values.
623
+ */
624
+ static dependencies(token) {
625
+ const dependencies = new Set();
626
+ const seen = new Set();
627
+ const walk = (value) => {
628
+ if (!isObject(value) || seen.has(value)) {
629
+ return;
630
+ }
631
+ seen.add(value);
632
+ if (isRef(value)) {
633
+ dependencies.add(tokenAt(token.scope.tokens, value[REF]));
634
+ }
635
+ else if (isComputed(value)) {
636
+ value[COMPUTED].args.forEach(walk);
637
+ }
638
+ else if (isPlain(value)) {
639
+ for (const key of Reflect.ownKeys(value)) {
640
+ walk(Reflect.get(value, key));
641
+ }
642
+ }
643
+ };
644
+ walk(token.raw);
645
+ return dependencies;
646
+ }
647
+ /**
648
+ * The tokens of the token's library whose configured values refer to it.
649
+ * The index is made from the library's tokens once, and again after a
650
+ * token is set to a different value.
651
+ */
652
+ static dependents(token) {
653
+ const version = configurationVersionOf(token.scope);
654
+ let index = dependentsIndexes.get(token.scope);
655
+ if (index?.version !== version) {
656
+ const dependents = new Map();
657
+ for (const entry of walkEntries(token.scope.tokens, [], {
658
+ whatToShow: "token",
659
+ })) {
660
+ const dependent = libraryTokenOf(entry.token);
661
+ for (const dependency of LibraryToken.dependencies(dependent)) {
662
+ const list = dependents.get(dependency);
663
+ if (list) {
664
+ list.push(dependent);
665
+ }
666
+ else {
667
+ dependents.set(dependency, [dependent]);
668
+ }
669
+ }
670
+ }
671
+ index = { version, dependents };
672
+ dependentsIndexes.set(token.scope, index);
249
673
  }
674
+ return index.dependents.get(token) ?? [];
250
675
  }
251
676
  }
677
+ const libraryTokenOf = (token) => {
678
+ if (!(token instanceof LibraryToken)) {
679
+ throw new Error(`"${token.name}" is not a token of a library.`);
680
+ }
681
+ return token;
682
+ };
683
+ /**
684
+ * A token's value as it was configured, or last given to `set()`, with its
685
+ * refs and computed values unresolved. A token of a library made by
686
+ * `extend()` that isn't given a value has its source token's.
687
+ *
688
+ * @remarks
689
+ * A token's `$value` is resolved, so it can't tell a value that is given from
690
+ * one that is derived from other tokens, or say where in a value a ref is. A
691
+ * tool that reads how a library is configured finds the refs in this value
692
+ * with {@link isRef} and the computed values with {@link isComputed}: a value
693
+ * with neither, at any depth, is given. A converter that writes a ref as a
694
+ * CSS `var()`, rather than its token's value, reads the ref here and its
695
+ * path with {@link pathOf}.
696
+ *
697
+ * It's the value the token holds, not a copy, so it isn't frozen. Changing it
698
+ * doesn't change the token: give a new value to `set()`. Refs and computed
699
+ * values are searched for in plain data, as the library resolves them, and
700
+ * not in a class instance or a function.
701
+ *
702
+ * @example
703
+ * ```ts
704
+ * const isGiven = (value: unknown): boolean =>
705
+ * !isRef(value) &&
706
+ * !isComputed(value) &&
707
+ * (typeof value !== "object" || value === null ||
708
+ * Object.values(value).every(isGiven));
709
+ *
710
+ * isGiven(configuredValueOf(library.tokens.colors.primary)); // true
711
+ * isGiven(configuredValueOf(library.tokens.colors.hover)); // false: a recipe
712
+ * ```
713
+ *
714
+ * @public
715
+ */
716
+ export const configuredValueOf = (token) => LibraryToken.configuredValue(libraryTokenOf(token));
717
+ /**
718
+ * The tokens that a token's value refers to, through refs anywhere in its
719
+ * configured value, including the arguments of the recipes it's computed
720
+ * with, in the token's library. Each token is listed once, in the order its
721
+ * first ref is in the value.
722
+ *
723
+ * @remarks
724
+ * These are the tokens the value is made from, and not theirs in turn: a
725
+ * tool follows each with `dependenciesOf()` for the whole chain. A value
726
+ * computed by a recipe from literal arguments only has no dependencies,
727
+ * though it's derived: {@link configuredValueOf} tells that apart.
728
+ *
729
+ * It throws when a ref's path isn't a token of the library, as reading the
730
+ * token's `$value` does.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * // hover: { $value: Recipes.darken(theme.colors.primary, 0.2) }
735
+ * dependenciesOf(library.tokens.colors.hover).map((token) => token.name);
736
+ * // ["colors.primary"]
737
+ * ```
738
+ *
739
+ * @public
740
+ */
741
+ export const dependenciesOf = (token) =>
742
+ // The token at a ref's path is a token of the library, of shape `R`: the
743
+ // config's types guarantee it.
744
+ Object.freeze([
745
+ ...LibraryToken.dependencies(libraryTokenOf(token)),
746
+ ]);
747
+ export function entriesOf(library, options = {}) {
748
+ return walkEntries(library.tokens, [], options);
749
+ }
750
+ function* walkEntries(node, path, options) {
751
+ const { whatToShow = "all", filter } = options;
752
+ for (const key of Object.keys(node)) {
753
+ if (!isChildKey(key)) {
754
+ continue;
755
+ }
756
+ const child = node[key];
757
+ const childPath = [...path, key];
758
+ const entry = isToken(child)
759
+ ? { kind: "token", path: childPath, token: child }
760
+ : { kind: "group", path: childPath, group: child };
761
+ let result = true;
762
+ if (whatToShow === "all" || whatToShow === entry.kind) {
763
+ result = filter?.(entry) ?? true;
764
+ if (result === true) {
765
+ yield entry;
766
+ }
767
+ }
768
+ if (entry.kind === "group" && result !== "reject") {
769
+ yield* walkEntries(child, childPath, options);
770
+ }
771
+ }
772
+ }
773
+ /**
774
+ * The tokens whose values refer to a token, through refs anywhere in their
775
+ * configured values, including the arguments of their recipes, in the
776
+ * token's library. It is the reverse of {@link dependenciesOf}: each token is
777
+ * listed once, in the order of the library.
778
+ *
779
+ * @remarks
780
+ * These are the tokens that refer to the token directly. A token whose value
781
+ * may change when this one is set is also one that refers to one of those: a
782
+ * tool follows each with `dependentsOf()` for the whole set, as it follows
783
+ * {@link dependenciesOf} for the chain the other way.
784
+ *
785
+ * This reads the configured values and not the values, so it resolves no
786
+ * token, and so it is known of a library whose tokens have not been read. It
787
+ * is found for the whole library once and kept until a token is set to a
788
+ * different value, so each call after the first is a lookup. A value a recipe
789
+ * reads other than through its arguments is not seen, as for
790
+ * {@link dependenciesOf}.
791
+ *
792
+ * It throws when a ref in a token of the library isn't a token of it, as
793
+ * reading the token's `$value` does.
794
+ *
795
+ * @example
796
+ * ```ts
797
+ * // hover: { $value: Recipes.darken(theme.colors.primary, 0.2) }
798
+ * dependentsOf(library.tokens.colors.primary).map((token) => token.name);
799
+ * // ["colors.hover"]
800
+ *
801
+ * // Every token that may change when a token is set
802
+ * const affected = new Set<Library.Token<any, any>>();
803
+ * const visit = (token: Library.Token<any, any>) => {
804
+ * for (const dependent of dependentsOf(token)) {
805
+ * if (!affected.has(dependent)) {
806
+ * affected.add(dependent);
807
+ * visit(dependent);
808
+ * }
809
+ * }
810
+ * };
811
+ * visit(extension.tokens.color.context);
812
+ * ```
813
+ *
814
+ * @public
815
+ */
816
+ export const dependentsOf = (token) =>
817
+ // The tokens that refer to a token are of its library, of shape `R`: the
818
+ // config's types guarantee it.
819
+ Object.freeze([
820
+ ...LibraryToken.dependents(libraryTokenOf(token)),
821
+ ]);
252
822
  //# sourceMappingURL=library.js.map