@chartguard/schema-generator 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/index.d.mts +6 -0
- package/dist/lib/chartjs-effects.d.mts +5 -0
- package/dist/lib/chartjs-plugins.d.mts +23 -0
- package/dist/lib/chartjs-scopes.d.mts +30 -0
- package/dist/lib/chartjs.d.mts +32 -0
- package/dist/lib/deprecation.d.mts +9 -0
- package/dist/lib/digest.d.mts +12 -0
- package/dist/lib/echarts-components.d.mts +2 -0
- package/dist/lib/echarts-deprecations.d.mts +5 -0
- package/dist/lib/echarts-handlers.d.mts +2 -0
- package/dist/lib/echarts-runtime.d.mts +2 -0
- package/dist/lib/echarts.d.mts +26 -0
- package/dist/lib/emitter.d.mts +173 -0
- package/dist/lib/generate.d.mts +14 -0
- package/dist/lib/highcharts-runtime.d.mts +2 -0
- package/dist/lib/highcharts.d.mts +26 -0
- package/dist/lib/local.d.mts +14 -0
- package/dist/lib/lock.d.mts +31 -0
- package/dist/lib/nodes.d.mts +4 -0
- package/dist/lib/paths.d.mts +22 -0
- package/dist/lib/program.d.mts +13 -0
- package/dist/lib/provenance.d.mts +46 -0
- package/dist/lib/runtime.d.mts +24 -0
- package/dist/lib/scriptable.d.mts +13 -0
- package/dist/lib/trace.d.mts +8 -0
- package/dist/lib/types.d.mts +52 -0
- package/index.mjs +20 -0
- package/lib/chartjs-effects.mjs +36 -0
- package/lib/chartjs-plugins.mjs +41 -0
- package/lib/chartjs-scopes.mjs +96 -0
- package/lib/chartjs.mjs +275 -0
- package/lib/deprecation.mjs +21 -0
- package/lib/digest.mjs +33 -0
- package/lib/echarts-components.mjs +176 -0
- package/lib/echarts-deprecations.mjs +206 -0
- package/lib/echarts-handlers.mjs +31 -0
- package/lib/echarts-runtime.mjs +311 -0
- package/lib/echarts.mjs +89 -0
- package/lib/emitter.mjs +333 -0
- package/lib/generate.mjs +35 -0
- package/lib/highcharts-runtime.mjs +198 -0
- package/lib/highcharts.mjs +105 -0
- package/lib/local.mjs +33 -0
- package/lib/lock.mjs +123 -0
- package/lib/nodes.mjs +17 -0
- package/lib/paths.mjs +50 -0
- package/lib/program.mjs +46 -0
- package/lib/provenance.mjs +100 -0
- package/lib/runtime.mjs +38 -0
- package/lib/scriptable.mjs +57 -0
- package/lib/trace.mjs +124 -0
- package/lib/types.mjs +74 -0
- package/package.json +47 -5
- package/upstream-sources.json +102 -0
package/lib/emitter.mjs
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
import { deprecationOf } from './deprecation.mjs';
|
|
6
|
+
import { Provenance } from './provenance.mjs';
|
|
7
|
+
|
|
8
|
+
const MAX_DEFINITIONS = 50_000;
|
|
9
|
+
/** Longer printed types are abbreviated with a content hash so distinct types never share a name. */
|
|
10
|
+
const NAME_LENGTH = 120;
|
|
11
|
+
const NAME_PREFIX = 80;
|
|
12
|
+
const NAME_FLAGS = ts.TypeFormatFlags.NoTruncation;
|
|
13
|
+
/** Platform objects (CanvasGradient, HTMLElement, …) are checked only as "some object". */
|
|
14
|
+
/** An object TypeScript's library declares: opaque; a platform class adds `host`, which a plain object never is. */
|
|
15
|
+
const HOST_OBJECT = { kind: 'object', additional: 'open', properties: {} };
|
|
16
|
+
/**
|
|
17
|
+
* TypeScript's global function interfaces declare no call signature, so they would pass for
|
|
18
|
+
* platform objects; a value of these types is a function (`(Function | Options)` in Highcharts).
|
|
19
|
+
*/
|
|
20
|
+
const GLOBAL_FUNCTIONS = new Set(['Function', 'CallableFunction', 'NewableFunction']);
|
|
21
|
+
/** A union whose emitted node is larger than this is stored once as a definition and referenced. */
|
|
22
|
+
const SHARED_UNION_BYTES = 300;
|
|
23
|
+
|
|
24
|
+
function isFlag(type, flag) {
|
|
25
|
+
return (type.flags & flag) !== 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function withoutUndefined(types) {
|
|
29
|
+
return types.filter((type) => !isFlag(type, ts.TypeFlags.Undefined | ts.TypeFlags.Void));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PRIMITIVE_FLAGS = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike;
|
|
33
|
+
|
|
34
|
+
function isPrimitiveLike(type) {
|
|
35
|
+
return isFlag(type, PRIMITIVE_FLAGS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Symbol-keyed members (`[Symbol.iterator]`) cannot occur in a configuration object. */
|
|
39
|
+
function isStringKeyed(symbol) {
|
|
40
|
+
return !symbol.escapedName.startsWith('__@');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A readable, collision-free definition name. The full printed type is kept when short;
|
|
45
|
+
* otherwise a prefix plus a hash of the whole text, so unrelated types never collapse into one name.
|
|
46
|
+
*/
|
|
47
|
+
function definitionName(printed) {
|
|
48
|
+
const name = printed.replace(/\s+/g, ' ').trim();
|
|
49
|
+
if (name.length <= NAME_LENGTH) return name;
|
|
50
|
+
const digest = createHash('sha256').update(name).digest('hex').slice(0, 10);
|
|
51
|
+
return `${name.slice(0, NAME_PREFIX)}…#${digest}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts TypeScript types into SchemaIR nodes. Object types become shared
|
|
56
|
+
* definitions so recursive and repeated types stay finite.
|
|
57
|
+
*/
|
|
58
|
+
export class TypeEmitter {
|
|
59
|
+
constructor({
|
|
60
|
+
program,
|
|
61
|
+
checker,
|
|
62
|
+
sourceId,
|
|
63
|
+
packageName,
|
|
64
|
+
opaque = new Set(),
|
|
65
|
+
open = new Set(),
|
|
66
|
+
registries = new Map(),
|
|
67
|
+
runtimeOptional = new Set(),
|
|
68
|
+
widened = new Map(),
|
|
69
|
+
wrappers = new Set()
|
|
70
|
+
}) {
|
|
71
|
+
this.program = program;
|
|
72
|
+
this.checker = checker;
|
|
73
|
+
this.provenance = new Provenance(checker, { sourceId, packageName });
|
|
74
|
+
/** Types whose contents the declarations do not describe (caller-chosen data). */
|
|
75
|
+
this.opaque = opaque;
|
|
76
|
+
/** Object types that are documented extension points. */
|
|
77
|
+
this.open = open;
|
|
78
|
+
/**
|
|
79
|
+
* Union type → `{ registry, provenance }`: the runtime registry its variants are looked up in,
|
|
80
|
+
* which plugins extend, so the union is extensible and checked against the caller's registry.
|
|
81
|
+
*/
|
|
82
|
+
this.registries = registries;
|
|
83
|
+
/** Property symbols the declarations require but the runtime defaults. */
|
|
84
|
+
this.runtimeOptional = runtimeOptional;
|
|
85
|
+
/**
|
|
86
|
+
* Property declaration node → extra IR nodes the runtime also accepts there. Keyed by the
|
|
87
|
+
* declaration, because a property reached through an intersection is a transient symbol.
|
|
88
|
+
*/
|
|
89
|
+
// A key without a declaration would widen every declaration-less property (keys a mapped type synthesizes).
|
|
90
|
+
if (widened.has(undefined)) throw new Error('widened: a property without a declaration cannot be widened');
|
|
91
|
+
this.widened = widened;
|
|
92
|
+
/** Alias names of homomorphic mapped wrappers (`DeepPartial`) that pass primitives through unchanged. */
|
|
93
|
+
this.wrappers = wrappers;
|
|
94
|
+
/** Type → builder of a replacement node, for positions the declarations do not describe faithfully. */
|
|
95
|
+
this.overrides = new Map();
|
|
96
|
+
this.definitions = {};
|
|
97
|
+
this.names = new Map();
|
|
98
|
+
this.usedNames = new Set();
|
|
99
|
+
this.sharedUnions = new Map();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
emit(type) {
|
|
103
|
+
const override = this.overrides.get(type);
|
|
104
|
+
if (override !== undefined) return override();
|
|
105
|
+
if (this.opaque.has(type)) return { kind: 'unknown' };
|
|
106
|
+
const primitive = this.emitPrimitive(type);
|
|
107
|
+
if (primitive !== undefined) return primitive;
|
|
108
|
+
if (type.isUnion()) return this.emitUnion(type);
|
|
109
|
+
if (isFlag(type, ts.TypeFlags.Object | ts.TypeFlags.Intersection)) return this.emitObjectLike(type);
|
|
110
|
+
return { kind: 'unknown' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
emitPrimitive(type) {
|
|
114
|
+
if (isFlag(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return { kind: 'unknown' };
|
|
115
|
+
if (isFlag(type, ts.TypeFlags.StringLike & ~ts.TypeFlags.StringLiteral)) return { kind: 'string' };
|
|
116
|
+
if (isFlag(type, ts.TypeFlags.NumberLike & ~ts.TypeFlags.NumberLiteral)) return { kind: 'number' };
|
|
117
|
+
if (type.isStringLiteral() || type.isNumberLiteral()) return { kind: 'literal', value: type.value };
|
|
118
|
+
if (isFlag(type, ts.TypeFlags.BooleanLiteral))
|
|
119
|
+
return { kind: 'literal', value: type === this.checker.getTrueType() };
|
|
120
|
+
if (isFlag(type, ts.TypeFlags.Null)) return { kind: 'null' };
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Distinct TypeScript types can emit the same node (`string[]` twice via different wrappers); keep one.
|
|
126
|
+
* A large union the package names (`AnimationEasing`) is emitted once as a definition and referenced.
|
|
127
|
+
*/
|
|
128
|
+
emitUnion(type) {
|
|
129
|
+
const node = this.buildUnion(type);
|
|
130
|
+
return node.kind === 'union' ? this.share(node, type) : node;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Large unions recur under many properties (an easing name list, a color union). Keyed by
|
|
135
|
+
* their emitted content, so the same union reached through `T` or an optional `T | undefined`
|
|
136
|
+
* is stored once; the first type that produced it names the definition.
|
|
137
|
+
*/
|
|
138
|
+
share(node, type) {
|
|
139
|
+
const content = JSON.stringify(node);
|
|
140
|
+
if (content.length <= SHARED_UNION_BYTES) return node;
|
|
141
|
+
const existing = this.sharedUnions.get(content);
|
|
142
|
+
if (existing !== undefined) return { kind: 'ref', ref: existing };
|
|
143
|
+
const printed = this.checker.typeToString(type, undefined, NAME_FLAGS).replace(/ \| undefined$/, '');
|
|
144
|
+
const name = this.uniqueName(definitionName(printed));
|
|
145
|
+
this.sharedUnions.set(content, name);
|
|
146
|
+
this.definitions[name] = node;
|
|
147
|
+
return { kind: 'ref', ref: name };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
buildUnion(type) {
|
|
151
|
+
const members = withoutUndefined(type.types);
|
|
152
|
+
const booleans = members.filter((member) => isFlag(member, ts.TypeFlags.BooleanLiteral));
|
|
153
|
+
const rest = members.filter((member) => !isFlag(member, ts.TypeFlags.BooleanLiteral));
|
|
154
|
+
const nodes = [...(booleans.length === 2 ? [{ kind: 'boolean' }] : booleans.map((b) => this.emit(b)))];
|
|
155
|
+
nodes.push(...rest.map((member) => this.emit(member)));
|
|
156
|
+
const unique = [...new Map(nodes.map((node) => [JSON.stringify(node), node])).values()];
|
|
157
|
+
if (unique.length === 1) return unique[0];
|
|
158
|
+
return this.withDiscriminator({ kind: 'union', anyOf: unique }, rest, this.registryOf(type));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Callers may name either `T` or the `T | undefined` of an optional property. */
|
|
162
|
+
registryOf(type) {
|
|
163
|
+
return this.registries.get(type) ?? this.registries.get(this.checker.getNonNullableType(type));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
withDiscriminator(union, members, registered) {
|
|
167
|
+
const discriminator = this.discriminatorOf(members);
|
|
168
|
+
if (discriminator === undefined) return union;
|
|
169
|
+
if (registered === undefined) return { ...union, discriminator };
|
|
170
|
+
return {
|
|
171
|
+
...union,
|
|
172
|
+
discriminator,
|
|
173
|
+
extensible: true,
|
|
174
|
+
registry: registered.registry,
|
|
175
|
+
...(registered.extensionPoint === undefined ? {} : { extensionPoint: registered.extensionPoint }),
|
|
176
|
+
provenance: registered.provenance
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A property present in every object member whose literal values are disjoint. Array members
|
|
182
|
+
* are left out: the engine tells arrays from objects by runtime kind, so `T | T[]` (which
|
|
183
|
+
* TypeScript flattens into one union) still discriminates its object members.
|
|
184
|
+
*/
|
|
185
|
+
discriminatorOf(members) {
|
|
186
|
+
const objects = members.filter((member) => !this.checker.isArrayType(member) && !this.checker.isTupleType(member));
|
|
187
|
+
if (
|
|
188
|
+
objects.length < 2 ||
|
|
189
|
+
!objects.every((member) => isFlag(member, ts.TypeFlags.Object | ts.TypeFlags.Intersection))
|
|
190
|
+
)
|
|
191
|
+
return undefined;
|
|
192
|
+
const [first] = objects;
|
|
193
|
+
const candidates = this.checker.getPropertiesOfType(first).map((symbol) => symbol.name);
|
|
194
|
+
return candidates.find((name) => this.isDiscriminator(objects, name));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
isDiscriminator(members, name) {
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const member of members) {
|
|
200
|
+
const symbol = this.checker.getPropertyOfType(member, name);
|
|
201
|
+
if (symbol === undefined) return false;
|
|
202
|
+
const type = this.checker.getTypeOfSymbol(symbol);
|
|
203
|
+
const literals = withoutUndefined(type.isUnion() ? type.types : [type]);
|
|
204
|
+
if (literals.length === 0 || !literals.every((literal) => literal.isStringLiteral())) return false;
|
|
205
|
+
for (const literal of literals) {
|
|
206
|
+
if (seen.has(literal.value)) return false;
|
|
207
|
+
seen.add(literal.value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* What TypeScript's default library declares a type to be, also when wrapped in a generic such as
|
|
215
|
+
* `DeepPartial<T>`: `{ host: name }` for a platform class (an interface with a global constructor:
|
|
216
|
+
* `Date`, `CanvasGradient`, `HTMLElement`), `{}` for an options dictionary it declares (`Intl.
|
|
217
|
+
* NumberFormatOptions`), opaque either way since the library's own types are no pinned source (D5);
|
|
218
|
+
* undefined for anything else. A mapped type (`Partial<T>`, `Pick`, `Omit`, `Record`) is declared
|
|
219
|
+
* there too, as an anonymous type literal, but it is an object with the properties it maps (D3, D6).
|
|
220
|
+
*/
|
|
221
|
+
libraryType(type) {
|
|
222
|
+
const symbol = type.getSymbol();
|
|
223
|
+
const declared = (symbol?.declarations ?? []).some((declaration) =>
|
|
224
|
+
this.program.isSourceFileDefaultLibrary(declaration.getSourceFile())
|
|
225
|
+
);
|
|
226
|
+
const named = symbol !== undefined && (symbol.flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.Class)) !== 0;
|
|
227
|
+
if (declared && named) {
|
|
228
|
+
const constructed = (symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Class)) !== 0;
|
|
229
|
+
return constructed ? { host: symbol.name } : {};
|
|
230
|
+
}
|
|
231
|
+
const [wrapped, ...rest] = type.aliasTypeArguments ?? [];
|
|
232
|
+
return wrapped !== undefined && rest.length === 0 ? this.libraryType(wrapped) : undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
emitObjectLike(type) {
|
|
236
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) return { kind: 'function' };
|
|
237
|
+
if (GLOBAL_FUNCTIONS.has(type.getSymbol()?.escapedName) && this.isLibraryDeclared(type.getSymbol()))
|
|
238
|
+
return { kind: 'function' };
|
|
239
|
+
if (this.checker.isArrayType(type) || this.checker.isTupleType(type)) return this.emitArray(type);
|
|
240
|
+
const unwrapped = this.emitUnwrapped(type);
|
|
241
|
+
if (unwrapped !== undefined) return unwrapped;
|
|
242
|
+
const library = this.libraryType(type);
|
|
243
|
+
if (library !== undefined) return this.reference(type, () => ({ ...HOST_OBJECT, ...library }));
|
|
244
|
+
return this.reference(type);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
isLibraryDeclared(symbol) {
|
|
248
|
+
return (symbol?.declarations ?? []).some((declaration) =>
|
|
249
|
+
this.program.isSourceFileDefaultLibrary(declaration.getSourceFile())
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Homomorphic wrappers such as `DeepPartial<T>` also map over primitives and arrays. A wrapped
|
|
255
|
+
* array is recognized by its numeric index plus the built-in `length`, and keeps its
|
|
256
|
+
* (partial) element type; a wrapped primitive is still that primitive. Only the wrappers
|
|
257
|
+
* named in `wrappers` qualify: `Wrap<T> = { v: T }` is an object, whatever `T` is.
|
|
258
|
+
*/
|
|
259
|
+
emitUnwrapped(type) {
|
|
260
|
+
const array = this.emitWrappedArray(type);
|
|
261
|
+
if (array !== undefined) return array;
|
|
262
|
+
if (!this.wrappers.has(type.aliasSymbol?.name)) return undefined;
|
|
263
|
+
const [wrapped, ...rest] = type.aliasTypeArguments ?? [];
|
|
264
|
+
if (wrapped === undefined || rest.length > 0) return undefined;
|
|
265
|
+
const members = wrapped.isIntersection() ? wrapped.types : [wrapped];
|
|
266
|
+
const primitive = members.find(isPrimitiveLike);
|
|
267
|
+
return primitive === undefined ? undefined : this.emit(primitive);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
emitWrappedArray(type) {
|
|
271
|
+
const element = this.checker.getIndexInfoOfType(type, ts.IndexKind.Number)?.type;
|
|
272
|
+
if (element === undefined || !this.isLibraryDeclared(this.checker.getPropertyOfType(type, 'length')))
|
|
273
|
+
return undefined;
|
|
274
|
+
return { kind: 'array', items: this.emit(element) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
emitArray(type) {
|
|
278
|
+
const elements = this.checker.getTypeArguments(type);
|
|
279
|
+
if (elements.length === 1) return { kind: 'array', items: this.emit(elements[0]) };
|
|
280
|
+
const items = this.checker.getUnionType(elements);
|
|
281
|
+
return { kind: 'array', items: this.emit(items) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** One shared definition per type, so repeated and recursive types stay finite. */
|
|
285
|
+
reference(type, build = () => this.emitObject(type)) {
|
|
286
|
+
const existing = this.names.get(type);
|
|
287
|
+
if (existing !== undefined) return { kind: 'ref', ref: existing };
|
|
288
|
+
if (this.names.size >= MAX_DEFINITIONS) return { kind: 'unknown' };
|
|
289
|
+
const name = this.uniqueName(definitionName(this.checker.typeToString(type, undefined, NAME_FLAGS)));
|
|
290
|
+
this.names.set(type, name);
|
|
291
|
+
this.definitions[name] = { kind: 'unknown' };
|
|
292
|
+
this.definitions[name] = build();
|
|
293
|
+
return { kind: 'ref', ref: name };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
uniqueName(base) {
|
|
297
|
+
let name = base;
|
|
298
|
+
for (let suffix = 2; this.usedNames.has(name); suffix++) name = `${base}~${String(suffix)}`;
|
|
299
|
+
this.usedNames.add(name);
|
|
300
|
+
return name;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
emitObject(type) {
|
|
304
|
+
const [index] = this.checker.getIndexInfosOfType(type);
|
|
305
|
+
const properties = this.checker.getPropertiesOfType(type).filter(isStringKeyed);
|
|
306
|
+
if (index !== undefined && properties.length === 0) return { kind: 'record', values: this.emit(index.type) };
|
|
307
|
+
const owner = this.provenance.ownerOf(type);
|
|
308
|
+
const entries = properties.map((symbol) => [symbol.name, this.emitProperty(symbol, owner)]);
|
|
309
|
+
return {
|
|
310
|
+
kind: 'object',
|
|
311
|
+
additional: index === undefined && !this.open.has(type) ? 'closed' : 'open',
|
|
312
|
+
provenance: this.provenance.of(owner),
|
|
313
|
+
properties: Object.fromEntries(entries)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Keys synthesized by mapped types have no declaration of their own; they cite their owner type. */
|
|
318
|
+
emitProperty(symbol, owner) {
|
|
319
|
+
const declared = this.emit(this.checker.getTypeOfSymbol(symbol));
|
|
320
|
+
const declaration = symbol.declarations?.[0];
|
|
321
|
+
const extra = declaration === undefined ? undefined : this.widened.get(declaration);
|
|
322
|
+
const node = extra === undefined ? declared : { kind: 'union', anyOf: [declared, ...extra] };
|
|
323
|
+
const optional = (symbol.flags & ts.SymbolFlags.Optional) !== 0 || this.runtimeOptional.has(symbol);
|
|
324
|
+
const provenance = this.provenance.of(declaration === undefined ? owner : symbol);
|
|
325
|
+
const deprecated = deprecationOf(symbol);
|
|
326
|
+
return {
|
|
327
|
+
node,
|
|
328
|
+
provenance,
|
|
329
|
+
...(optional ? {} : { required: true }),
|
|
330
|
+
...(deprecated === undefined ? {} : { deprecated })
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
package/lib/generate.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { checkArtifact } from '@chartguard/config-core';
|
|
2
|
+
|
|
3
|
+
import { generateChartjs } from './chartjs.mjs';
|
|
4
|
+
import { serialize, withDigest } from './digest.mjs';
|
|
5
|
+
import { generateEcharts } from './echarts.mjs';
|
|
6
|
+
import { generateHighcharts } from './highcharts.mjs';
|
|
7
|
+
import { untracedFacts } from './trace.mjs';
|
|
8
|
+
|
|
9
|
+
const GENERATORS = { chartjs: generateChartjs, echarts: generateEcharts, highcharts: generateHighcharts };
|
|
10
|
+
|
|
11
|
+
/** The libraries this generator builds schema artifacts for: `chartjs`, `echarts`, `highcharts`. */
|
|
12
|
+
export const LIBRARIES = Object.keys(GENERATORS);
|
|
13
|
+
|
|
14
|
+
/** Upstream licenses under which a derived artifact may be committed and published. */
|
|
15
|
+
const REDISTRIBUTABLE = new Set(['allowed-with-attribution', 'allowed-with-license-and-notice']);
|
|
16
|
+
|
|
17
|
+
/** Whether the pin's upstream license lets an artifact derived from it be committed and published. */
|
|
18
|
+
export function isRedistributable(pin) {
|
|
19
|
+
return REDISTRIBUTABLE.has(pin.license.redistribution);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The artifact for `library` from the install under `root`, as deterministic text carrying the
|
|
24
|
+
* digest of its own content; never a malformed one, nor one with a fact that does not trace to its source.
|
|
25
|
+
*/
|
|
26
|
+
export function generateArtifact(root, library, pin) {
|
|
27
|
+
const artifact = withDigest(GENERATORS[library](root, pin));
|
|
28
|
+
const { problems } = checkArtifact(artifact);
|
|
29
|
+
if (problems.length > 0)
|
|
30
|
+
throw new Error(`${library}: generated artifact is malformed:\n${problems.slice(0, 20).join('\n')}`);
|
|
31
|
+
const untraced = untracedFacts(root, artifact);
|
|
32
|
+
if (untraced.length > 0)
|
|
33
|
+
throw new Error(`${library}: generated facts do not trace to their source:\n${untraced.slice(0, 20).join('\n')}`);
|
|
34
|
+
return { artifact, text: serialize(artifact) };
|
|
35
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { sameNode, upstreamChanged } from './nodes.mjs';
|
|
2
|
+
import { runtimePointer } from './runtime.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What the Highcharts declarations leave narrower than the runtime, found by running the official
|
|
6
|
+
* demos (CG-01-05 part 4) and read from the pinned runtime code, each fact citing its line:
|
|
7
|
+
*
|
|
8
|
+
* - options are read with `pick()`, which skips `null` like `undefined`: a `null` optional option
|
|
9
|
+
* is unset (`chart.plotBackgroundColor: null`, `yAxis.title.text: null` in the demos);
|
|
10
|
+
* - `dashstyleSetter` lower-cases the value and turns words and comma lists into a dash array;
|
|
11
|
+
* - category labels are `categories[pos] ?? names[pos] ?? pos`: numbers label as well as strings;
|
|
12
|
+
* - `optionsToObject` maps a number, `null` or any array to a point by the series' `keys` or
|
|
13
|
+
* `pointArrayMap`, whatever tuples the declarations list; `getX` parses a string `x` as a date,
|
|
14
|
+
* and x-range points parse `x2` the same way and take `partialFill` as a bare amount;
|
|
15
|
+
* - radial axes take `labels.rotation: 'auto'`; organization charts, like every sankey-based
|
|
16
|
+
* series, take `nodeWidth: 'auto'`; `time.useUTC` is a boolean, as its own documentation says;
|
|
17
|
+
* - `dataMapping` keys are point properties (the declaration is an empty interface that says so);
|
|
18
|
+
* - a text's `style.width` is read with `pInt`, so a CSS length (`'90px'`) is a width as well as a number;
|
|
19
|
+
* - `plotOptions.series` is merged into every series, so it takes the options of every series type.
|
|
20
|
+
*/
|
|
21
|
+
const FACTS = {
|
|
22
|
+
pick: { file: 'highcharts.src.js', pattern: "if (typeof arg !== 'undefined' && arg !== null) {" },
|
|
23
|
+
dashStyle: { file: 'highcharts.src.js', pattern: 'value = value.toLowerCase();' },
|
|
24
|
+
categories: { file: 'highcharts.src.js', pattern: '(categories[pos] ?? names[pos] ?? pos) :' },
|
|
25
|
+
points: { file: 'highcharts.src.js', pattern: 'pointArrayMap = keys || series.pointArrayMap' },
|
|
26
|
+
pointX: { file: 'highcharts.src.js', pattern: '// If x is a string, try to parse it to a datetime' },
|
|
27
|
+
pointX2: { file: 'modules/xrange.src.js', pattern: 'this.x2 = this.series.chart.time.parse(this.x2);' },
|
|
28
|
+
partialFill: { file: 'modules/xrange.src.js', pattern: 'partialFill = point.partialFill ?? 0;' },
|
|
29
|
+
rotation: { file: 'highcharts-more.src.js', pattern: "if (labelOptions.rotation === 'auto') {" },
|
|
30
|
+
nodeWidth: { file: 'modules/sankey.src.js', pattern: "if (nodeWidth === 'auto') {" },
|
|
31
|
+
useUTC: { file: 'highcharts.d.ts', pattern: 'Setting `useUTC` to true is equivalent to setting `time.timezone` to' },
|
|
32
|
+
dataMapping: { file: 'highcharts.d.ts', pattern: 'export interface DataMappingOptionsObject {}' },
|
|
33
|
+
plotSeries: { file: 'highcharts.src.js', pattern: 'const options = merge(typeOptions, plotOptions.series,' },
|
|
34
|
+
cssWidth: { file: 'highcharts.src.js', pattern: 'textWidth = this.textWidth = pInt(styles.width);' }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ANY_ARRAY = { kind: 'array', items: { kind: 'unknown' } };
|
|
38
|
+
const changed = upstreamChanged('highcharts types');
|
|
39
|
+
|
|
40
|
+
/** The provenance with one more runtime source; a node without provenance gets the runtime fact as its own. */
|
|
41
|
+
const withSource = (provenance, source) =>
|
|
42
|
+
provenance === undefined
|
|
43
|
+
? { confidence: 'authoritative', sources: [source] }
|
|
44
|
+
: { ...provenance, sources: [...provenance.sources, source] };
|
|
45
|
+
|
|
46
|
+
/** `node` also accepting `extra` members it does not have yet. */
|
|
47
|
+
function widened(node, extra) {
|
|
48
|
+
const members = node.kind === 'union' ? node.anyOf : [node];
|
|
49
|
+
const added = extra.filter((member) => !members.some((known) => sameNode(known, member)));
|
|
50
|
+
return added.length === 0 ? node : { kind: 'union', anyOf: [...members, ...added] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A step: the `key` property of the definitions `owners` names, transformed and citing the fact. */
|
|
54
|
+
function property(owners, key, transform) {
|
|
55
|
+
return (definitions, source) => {
|
|
56
|
+
const changes = owners(definitions).flatMap((name) => {
|
|
57
|
+
const definition = definitions[name];
|
|
58
|
+
const known = definition?.properties?.[key];
|
|
59
|
+
if (known === undefined) throw changed(`${name}.${key}`);
|
|
60
|
+
const next = { ...known, node: transform(known.node), provenance: withSource(known.provenance, source) };
|
|
61
|
+
return [[name, { ...definition, properties: { ...definition.properties, [key]: next } }]];
|
|
62
|
+
});
|
|
63
|
+
return { ...definitions, ...Object.fromEntries(changes) };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Object definitions with a `key` property whose definition name and node pass `test`. */
|
|
68
|
+
const objectsWith =
|
|
69
|
+
(key, test = () => true) =>
|
|
70
|
+
(definitions) =>
|
|
71
|
+
Object.keys(definitions).filter((name) => {
|
|
72
|
+
const known = definitions[name].kind === 'object' ? definitions[name].properties[key] : undefined;
|
|
73
|
+
return known !== undefined && test(name, known.node);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Every object definition: a null optional option is unset. */
|
|
77
|
+
function nullAsAbsent(definitions, source) {
|
|
78
|
+
const entries = Object.entries(definitions).map(([name, node]) =>
|
|
79
|
+
node.kind === 'object'
|
|
80
|
+
? [name, { ...node, nullAsAbsent: true, provenance: withSource(node.provenance, source) }]
|
|
81
|
+
: [name, node]
|
|
82
|
+
);
|
|
83
|
+
return Object.fromEntries(entries);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A list of points: a number, `null` and any array are points too; object members stay as declared. */
|
|
87
|
+
function points(node) {
|
|
88
|
+
if (node.kind !== 'array') return node;
|
|
89
|
+
const members = node.items.kind === 'union' ? node.items.anyOf : [node.items];
|
|
90
|
+
const declared = members.filter((member) => member.kind !== 'array');
|
|
91
|
+
return {
|
|
92
|
+
kind: 'array',
|
|
93
|
+
items: widened({ kind: 'union', anyOf: declared }, [{ kind: 'number' }, { kind: 'null' }, ANY_ARRAY])
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const categories = (node) =>
|
|
98
|
+
node.kind === 'array' ? { kind: 'array', items: widened(node.items, [{ kind: 'number' }]) } : node;
|
|
99
|
+
|
|
100
|
+
function dashStyle(definitions, source) {
|
|
101
|
+
const union = definitions.DashStyleValue;
|
|
102
|
+
if (union?.kind !== 'union' || !union.anyOf.every((member) => member.kind === 'literal'))
|
|
103
|
+
throw changed('DashStyleValue');
|
|
104
|
+
return { ...definitions, DashStyleValue: { kind: 'string', provenance: withSource(union.provenance, source) } };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dataMapping(definitions, source) {
|
|
108
|
+
const mapping = definitions.DataMappingOptionsObject;
|
|
109
|
+
if (mapping?.kind !== 'object' || Object.keys(mapping.properties).length > 0)
|
|
110
|
+
throw changed('DataMappingOptionsObject');
|
|
111
|
+
return {
|
|
112
|
+
...definitions,
|
|
113
|
+
DataMappingOptionsObject: { ...mapping, additional: 'open', provenance: withSource(mapping.provenance, source) }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `plotOptions.series`: its own options, then every option of every series type, a name taking each distinct type. */
|
|
118
|
+
function anySeriesOptions(definitions, plotOptions, source) {
|
|
119
|
+
const own = definitions[plotOptions.properties.series.node.ref];
|
|
120
|
+
const collected = new Map(
|
|
121
|
+
Object.entries(own.properties).map(([key, known]) => [key, { known, nodes: [known.node] }])
|
|
122
|
+
);
|
|
123
|
+
for (const [key, entry] of Object.entries(plotOptions.properties)) {
|
|
124
|
+
const options = key === 'series' || entry.node.kind !== 'ref' ? undefined : definitions[entry.node.ref];
|
|
125
|
+
for (const [name, known] of Object.entries(options?.properties ?? {})) {
|
|
126
|
+
const found = collected.get(name) ?? { known, nodes: [] };
|
|
127
|
+
if (!found.nodes.some((node) => sameNode(node, known.node))) found.nodes.push(known.node);
|
|
128
|
+
collected.set(name, found);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const properties = [...collected].map(([name, { known, nodes }]) => [
|
|
132
|
+
name,
|
|
133
|
+
{
|
|
134
|
+
...known,
|
|
135
|
+
node: nodes.length === 1 ? nodes[0] : { kind: 'union', anyOf: nodes },
|
|
136
|
+
provenance: withSource(known.provenance, source)
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
return { ...own, properties: Object.fromEntries(properties) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `plotOptions.series` pointing at the merged options of every series type. */
|
|
143
|
+
function anySeries(definitions, optionName, source) {
|
|
144
|
+
const name = definitions[optionName].properties.plotOptions.node.ref;
|
|
145
|
+
const plotOptions = definitions[name];
|
|
146
|
+
const series = { ...plotOptions.properties.series, node: { kind: 'ref', ref: 'AnySeriesPlotOptions' } };
|
|
147
|
+
return {
|
|
148
|
+
...definitions,
|
|
149
|
+
AnySeriesPlotOptions: anySeriesOptions(definitions, plotOptions, source),
|
|
150
|
+
[name]: { ...plotOptions, properties: { ...plotOptions.properties, series } }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A step: the `key` property of the definitions `owners` names also accepts `extra`. */
|
|
155
|
+
const alsoAccepts = (owners, key, ...extra) => property(owners, key, (node) => widened(node, extra));
|
|
156
|
+
const named =
|
|
157
|
+
(...names) =>
|
|
158
|
+
() =>
|
|
159
|
+
names;
|
|
160
|
+
const STRING = { kind: 'string' };
|
|
161
|
+
|
|
162
|
+
/** The facts in the order they apply; later steps see what earlier ones produced. */
|
|
163
|
+
function steps({ optionName, seriesVariants }) {
|
|
164
|
+
const isPointX = (name, node) => name.endsWith('PointOptionsObject') && node.kind === 'number';
|
|
165
|
+
// Indicator series (`abands`, …) take their data from `linkedTo` and declare none.
|
|
166
|
+
const withData = (defs) => seriesVariants.filter((name) => defs[name].properties.data !== undefined);
|
|
167
|
+
return [
|
|
168
|
+
['pick', nullAsAbsent],
|
|
169
|
+
[
|
|
170
|
+
'categories',
|
|
171
|
+
property(
|
|
172
|
+
objectsWith('categories', (_, node) => node.kind === 'array'),
|
|
173
|
+
'categories',
|
|
174
|
+
categories
|
|
175
|
+
)
|
|
176
|
+
],
|
|
177
|
+
['points', property(withData, 'data', points)],
|
|
178
|
+
['pointX', alsoAccepts(objectsWith('x', isPointX), 'x', STRING)],
|
|
179
|
+
['pointX2', alsoAccepts(named('XrangePointOptionsObject'), 'x2', STRING)],
|
|
180
|
+
['partialFill', alsoAccepts(named('XrangePointOptionsObject'), 'partialFill', { kind: 'number' })],
|
|
181
|
+
[
|
|
182
|
+
'rotation',
|
|
183
|
+
alsoAccepts(named('XAxisLabelsOptions', 'YAxisLabelsOptions'), 'rotation', { kind: 'literal', value: 'auto' })
|
|
184
|
+
],
|
|
185
|
+
['nodeWidth', alsoAccepts(objectsWith('nodeWidth'), 'nodeWidth', STRING)],
|
|
186
|
+
['useUTC', alsoAccepts((defs) => [defs[optionName].properties.time.node.ref], 'useUTC', { kind: 'boolean' })],
|
|
187
|
+
['dashStyle', dashStyle],
|
|
188
|
+
['dataMapping', dataMapping],
|
|
189
|
+
['cssWidth', alsoAccepts(named('CSSObject'), 'width', STRING)],
|
|
190
|
+
['plotSeries', (defs, source) => anySeries(defs, optionName, source)]
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** New definitions with the runtime facts above applied, one step after the other, each citing its line. */
|
|
195
|
+
export function withHighchartsRuntimeFacts(root, definitions, shape) {
|
|
196
|
+
const cite = (fact) => ({ source: 'highcharts', pointer: runtimePointer(root, 'highcharts', FACTS[fact]) });
|
|
197
|
+
return steps(shape).reduce((defs, [fact, step]) => step(defs, cite(fact)), definitions);
|
|
198
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { omit } from './digest.mjs';
|
|
2
|
+
import { TypeEmitter } from './emitter.mjs';
|
|
3
|
+
import { withHighchartsRuntimeFacts } from './highcharts-runtime.mjs';
|
|
4
|
+
import { upstreamChanged } from './nodes.mjs';
|
|
5
|
+
import { createEntryProgram, exportedAliasTypes } from './program.mjs';
|
|
6
|
+
import { runtimePointer, runtimeSection } from './runtime.mjs';
|
|
7
|
+
import { artifactOf } from './types.mjs';
|
|
8
|
+
|
|
9
|
+
const SOURCE_ID = 'highcharts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `Options` from `highcharts.d.ts`, which imports the option declarations of every series type
|
|
13
|
+
* (`options/*.d.ts`): the full option set, like the ECharts full bundle.
|
|
14
|
+
*/
|
|
15
|
+
const ENTRY = ["import type { Options } from 'highcharts';", 'export type Option = Options;'].join('\n');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `Chart#initSeries` resolves `options.type || chart.type` in the series registry and throws error
|
|
19
|
+
* #17 for a type no loaded module registered. A series without `type` (the declarations'
|
|
20
|
+
* `UnknownSeriesOptions`) takes `chart.type`, which `ChartDefaults` sets to `'line'`.
|
|
21
|
+
*/
|
|
22
|
+
const SERIES_LOOKUP = { file: 'highcharts.src.js', pattern: 'error(17, true, chart, { missingModuleFor: type });' };
|
|
23
|
+
const SERIES_TYPE = {
|
|
24
|
+
file: 'highcharts.src.js',
|
|
25
|
+
pattern: 'optionsChart.type), SeriesClass = Chart_seriesTypes[type];'
|
|
26
|
+
};
|
|
27
|
+
const CHART_DEFAULTS = { file: 'highcharts.src.js', pattern: 'const ChartDefaults = {', end: '};' };
|
|
28
|
+
/** A member of the `ChartDefaults` object itself, not of a nested one: four spaces of indentation. */
|
|
29
|
+
const DEFAULT_TYPE = /^ {4}type: '(\w+)',$/;
|
|
30
|
+
|
|
31
|
+
const changed = upstreamChanged('highcharts types');
|
|
32
|
+
|
|
33
|
+
function typeLiteral(definitions, member) {
|
|
34
|
+
const target = member.kind === 'ref' ? definitions[member.ref] : member;
|
|
35
|
+
const type = target?.kind === 'object' ? target.properties.type?.node : undefined;
|
|
36
|
+
return type?.kind === 'literal' ? type.value : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The runtime's default chart type, and the lines that make it the default of a series without type. */
|
|
40
|
+
function seriesDefault(root) {
|
|
41
|
+
const defaults = runtimeSection(root, 'highcharts', CHART_DEFAULTS);
|
|
42
|
+
const index = defaults.lines.findIndex((line) => DEFAULT_TYPE.test(line));
|
|
43
|
+
if (index === -1) throw new Error(`highcharts runtime changed: no default chart type at ${defaults.pointer}`);
|
|
44
|
+
const pointers = [
|
|
45
|
+
runtimePointer(root, 'highcharts', SERIES_LOOKUP),
|
|
46
|
+
runtimePointer(root, 'highcharts', SERIES_TYPE),
|
|
47
|
+
defaults.pointerAt(index)
|
|
48
|
+
];
|
|
49
|
+
return {
|
|
50
|
+
fallback: DEFAULT_TYPE.exec(defaults.lines[index])[1],
|
|
51
|
+
provenance: { confidence: 'authoritative', sources: pointers.map((pointer) => ({ source: SOURCE_ID, pointer })) }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A series variant whose `type` is no longer required: the runtime takes it from `chart.type`
|
|
57
|
+
* (the declarations say so themselves with `UnknownSeriesOptions = Omit<…, "type">`).
|
|
58
|
+
*/
|
|
59
|
+
function withOptionalType(variant, provenance) {
|
|
60
|
+
const type = { ...omit(variant.properties.type, ['required']), provenance };
|
|
61
|
+
return { ...variant, properties: { ...variant.properties, type } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The series item union: one variant per registered series type, told apart by `type`, which defaults to chart.type. */
|
|
65
|
+
function seriesUnion(root, definitions, option) {
|
|
66
|
+
const name = option.properties.series?.node.items?.ref;
|
|
67
|
+
const union = definitions[name];
|
|
68
|
+
if (union?.kind !== 'union') throw changed('series is no longer Array<SeriesOptionsType | UnknownSeriesOptions>');
|
|
69
|
+
const typed = union.anyOf.filter((member) => typeLiteral(definitions, member) !== undefined);
|
|
70
|
+
if (union.anyOf.length - typed.length !== 1)
|
|
71
|
+
throw changed('expected exactly one series member without a type literal');
|
|
72
|
+
if (typed.some((member) => member.kind !== 'ref')) throw changed('expected every series variant to be a definition');
|
|
73
|
+
const { fallback, provenance } = seriesDefault(root);
|
|
74
|
+
const variants = Object.fromEntries(
|
|
75
|
+
typed.map((member) => [member.ref, withOptionalType(definitions[member.ref], provenance)])
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
variants,
|
|
80
|
+
definition: {
|
|
81
|
+
kind: 'union',
|
|
82
|
+
anyOf: typed,
|
|
83
|
+
discriminator: 'type',
|
|
84
|
+
extensible: true,
|
|
85
|
+
defaultFrom: { path: ['chart', 'type'], fallback },
|
|
86
|
+
registry: { id: 'highcharts.series', prefix: '', enforced: true },
|
|
87
|
+
extensionPoint: 'highcharts.series',
|
|
88
|
+
provenance
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function generateHighcharts(root, pin) {
|
|
94
|
+
const { program, checker, entry } = createEntryProgram(root, ENTRY);
|
|
95
|
+
const aliases = exportedAliasTypes(checker, entry);
|
|
96
|
+
const emitter = new TypeEmitter({ program, checker, sourceId: SOURCE_ID, packageName: pin.package });
|
|
97
|
+
const optionRef = emitter.reference(aliases.get('Option'));
|
|
98
|
+
const series = seriesUnion(root, emitter.definitions, emitter.definitions[optionRef.ref]);
|
|
99
|
+
const declared = { ...emitter.definitions, ...series.variants, [series.name]: series.definition };
|
|
100
|
+
const definitions = withHighchartsRuntimeFacts(root, declared, {
|
|
101
|
+
optionName: optionRef.ref,
|
|
102
|
+
seriesVariants: Object.keys(series.variants)
|
|
103
|
+
});
|
|
104
|
+
return artifactOf({ library: 'highcharts', sourceId: SOURCE_ID, pin, definitions, root: optionRef });
|
|
105
|
+
}
|