@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -2
  3. package/dist/index.d.mts +6 -0
  4. package/dist/lib/chartjs-effects.d.mts +5 -0
  5. package/dist/lib/chartjs-plugins.d.mts +23 -0
  6. package/dist/lib/chartjs-scopes.d.mts +30 -0
  7. package/dist/lib/chartjs.d.mts +32 -0
  8. package/dist/lib/deprecation.d.mts +9 -0
  9. package/dist/lib/digest.d.mts +12 -0
  10. package/dist/lib/echarts-components.d.mts +2 -0
  11. package/dist/lib/echarts-deprecations.d.mts +5 -0
  12. package/dist/lib/echarts-handlers.d.mts +2 -0
  13. package/dist/lib/echarts-runtime.d.mts +2 -0
  14. package/dist/lib/echarts.d.mts +26 -0
  15. package/dist/lib/emitter.d.mts +173 -0
  16. package/dist/lib/generate.d.mts +14 -0
  17. package/dist/lib/highcharts-runtime.d.mts +2 -0
  18. package/dist/lib/highcharts.d.mts +26 -0
  19. package/dist/lib/local.d.mts +14 -0
  20. package/dist/lib/lock.d.mts +31 -0
  21. package/dist/lib/nodes.d.mts +4 -0
  22. package/dist/lib/paths.d.mts +22 -0
  23. package/dist/lib/program.d.mts +13 -0
  24. package/dist/lib/provenance.d.mts +46 -0
  25. package/dist/lib/runtime.d.mts +24 -0
  26. package/dist/lib/scriptable.d.mts +13 -0
  27. package/dist/lib/trace.d.mts +8 -0
  28. package/dist/lib/types.d.mts +52 -0
  29. package/index.mjs +20 -0
  30. package/lib/chartjs-effects.mjs +36 -0
  31. package/lib/chartjs-plugins.mjs +41 -0
  32. package/lib/chartjs-scopes.mjs +96 -0
  33. package/lib/chartjs.mjs +275 -0
  34. package/lib/deprecation.mjs +21 -0
  35. package/lib/digest.mjs +33 -0
  36. package/lib/echarts-components.mjs +176 -0
  37. package/lib/echarts-deprecations.mjs +206 -0
  38. package/lib/echarts-handlers.mjs +31 -0
  39. package/lib/echarts-runtime.mjs +311 -0
  40. package/lib/echarts.mjs +89 -0
  41. package/lib/emitter.mjs +333 -0
  42. package/lib/generate.mjs +35 -0
  43. package/lib/highcharts-runtime.mjs +198 -0
  44. package/lib/highcharts.mjs +105 -0
  45. package/lib/local.mjs +33 -0
  46. package/lib/lock.mjs +123 -0
  47. package/lib/nodes.mjs +17 -0
  48. package/lib/paths.mjs +50 -0
  49. package/lib/program.mjs +46 -0
  50. package/lib/provenance.mjs +100 -0
  51. package/lib/runtime.mjs +38 -0
  52. package/lib/scriptable.mjs +57 -0
  53. package/lib/trace.mjs +124 -0
  54. package/lib/types.mjs +74 -0
  55. package/package.json +47 -5
  56. package/upstream-sources.json +102 -0
@@ -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
+ }
@@ -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
+ }