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.
- package/CHANGELOG.md +79 -1
- package/README.md +39 -3
- package/dist/lib/css-reflector.d.ts +230 -9
- package/dist/lib/css-reflector.js +217 -29
- package/dist/lib/css-reflector.js.map +1 -1
- package/dist/lib/design-token.d.ts +225 -46
- package/dist/lib/design-token.js +1 -1
- package/dist/lib/design-token.js.map +1 -1
- package/dist/lib/index.d.ts +5 -2
- package/dist/lib/index.js +5 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/library.d.ts +581 -28
- package/dist/lib/library.js +695 -125
- package/dist/lib/library.js.map +1 -1
- package/dist/lib/notifier.js.map +1 -1
- package/dist/lib/public.d.ts +1405 -0
- package/dist/lib/queue.js +10 -1
- package/dist/lib/queue.js.map +1 -1
- package/dist/lib/recipe.d.ts +81 -0
- package/dist/lib/recipe.js +101 -0
- package/dist/lib/recipe.js.map +1 -0
- package/dist/lib/ref.d.ts +91 -0
- package/dist/lib/ref.js +114 -0
- package/dist/lib/ref.js.map +1 -0
- package/dist/lib/tsconfig.tsbuildinfo +1 -0
- package/dist/lib/tsdoc-metadata.json +11 -0
- package/dist/lib/utilities.d.ts +10 -4
- package/dist/lib/utilities.js +10 -0
- package/dist/lib/utilities.js.map +1 -1
- package/dist/lib/values.d.ts +34 -0
- package/dist/lib/values.js +64 -0
- package/dist/lib/values.js.map +1 -0
- package/dist/lib/watcher.js.map +1 -1
- package/package.json +53 -40
- package/design-token-library-0.0.0.tgz +0 -0
package/dist/lib/library.js
CHANGED
|
@@ -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
|
-
*
|
|
72
|
+
* Names beginning with `$` are for format properties, except `$root`: the
|
|
73
|
+
* reserved name of a group's own token.
|
|
22
74
|
*/
|
|
23
|
-
const
|
|
24
|
-
return
|
|
75
|
+
const isChildKey = (key) => {
|
|
76
|
+
return !key.startsWith("$") || key === "$root";
|
|
25
77
|
};
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
|
|
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,
|
|
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
|
|
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,
|
|
45
|
-
Object.freeze(
|
|
134
|
+
recurseCreate(_name, group, config[key], scope, typeContext, queue);
|
|
135
|
+
Object.freeze(group);
|
|
46
136
|
}
|
|
47
137
|
else if (isToken(config[key])) {
|
|
48
|
-
const
|
|
49
|
-
|
|
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,
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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 (
|
|
89
|
-
const token =
|
|
90
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 (
|
|
140
|
-
|
|
377
|
+
if (isRef(value)) {
|
|
378
|
+
return tokenAt(scope.tokens, value[REF]).$value;
|
|
141
379
|
}
|
|
142
|
-
if (
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
426
|
+
scope;
|
|
153
427
|
queue;
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
457
|
+
extend(config, options = {}) {
|
|
458
|
+
assertNoRoot(config);
|
|
166
459
|
const queue = new Queue();
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
492
|
+
scope;
|
|
185
493
|
_description;
|
|
186
|
-
|
|
494
|
+
_deprecated;
|
|
187
495
|
queue;
|
|
188
496
|
raw;
|
|
189
497
|
cached = empty;
|
|
498
|
+
resolving = false;
|
|
499
|
+
notifying = false;
|
|
190
500
|
subscriptions = new Set();
|
|
191
|
-
|
|
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.
|
|
510
|
+
this.scope = scope;
|
|
195
511
|
this._description = _description;
|
|
196
|
-
this.
|
|
512
|
+
this._deprecated = _deprecated;
|
|
197
513
|
this.queue = queue;
|
|
198
514
|
this.raw = value;
|
|
199
|
-
|
|
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.
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|