@chartguard/schema-generator 0.0.0-stage → 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -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,52 @@
1
+ /**
2
+ * Checked lookups into upstream declarations. Each throws with the library name when the
3
+ * declarations no longer have the expected shape, so an upstream change fails generation
4
+ * loudly instead of producing a silently different artifact.
5
+ */
6
+ export function typeLookups(checker: any, library: any): {
7
+ checker: any;
8
+ property: (type: any, name: any) => any;
9
+ propertyType: (type: any, name: any) => any;
10
+ indexValueType: (type: any) => any;
11
+ elementType: (arrayType: any) => any;
12
+ };
13
+ /** The members of a union type, or the type itself. */
14
+ export function unionMembers(type: any): any;
15
+ /**
16
+ * The artifact header shared by every generator; only the library-specific parts differ. It names
17
+ * everything the artifact is built from: the library, the compiler that read it, and the generator.
18
+ */
19
+ export function artifactOf({ library, sourceId, pin, definitions, root }: {
20
+ library: any;
21
+ sourceId: any;
22
+ pin: any;
23
+ definitions: any;
24
+ root: any;
25
+ }): {
26
+ irVersion: number;
27
+ library: any;
28
+ libraryVersion: any;
29
+ coverage: string;
30
+ generator: {
31
+ name: any;
32
+ version: any;
33
+ };
34
+ sources: {
35
+ [sourceId]: {
36
+ kind: string;
37
+ locator: string;
38
+ integrity: any;
39
+ license: any;
40
+ };
41
+ typescript: {
42
+ kind: string;
43
+ locator: string;
44
+ integrity: any;
45
+ license: any;
46
+ };
47
+ };
48
+ definitions: any;
49
+ root: any;
50
+ };
51
+ /** The source id of the TypeScript compiler, whose lib files facts about DOM and built-in types cite. */
52
+ export const COMPILER_SOURCE: "typescript";
package/index.mjs ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Generates ChartGuard schema artifacts from the chart libraries' own published declarations and
3
+ * runtime code. `root` is always the project whose `node_modules` holds the library; nothing from
4
+ * it is executed, only read.
5
+ */
6
+ export { digestOf, omit, serialize, withDigest } from './lib/digest.mjs';
7
+ export { generateArtifact, isRedistributable, LIBRARIES } from './lib/generate.mjs';
8
+ export { buildLocalSchema } from './lib/local.mjs';
9
+ export {
10
+ compilerPin,
11
+ contentIntegrity,
12
+ installedUpstream,
13
+ pinOf,
14
+ sourceDirectory,
15
+ UpstreamError,
16
+ verifiedCompiler,
17
+ verifiedUpstream
18
+ } from './lib/lock.mjs';
19
+ export { LOCAL_ARTIFACTS, localSchemaPath } from './lib/paths.mjs';
20
+ export { untracedFacts } from './lib/trace.mjs';
@@ -0,0 +1,36 @@
1
+ import { runtimePointer } from './runtime.mjs';
2
+
3
+ /**
4
+ * What Chart.js does with an option of another type than the declarations say (A73), read from the
5
+ * line each fact cites. `_checkEventBindings` compares `responsive` with `!==` to whether resize
6
+ * listeners are bound (a boolean): any other value never matches, so every update unbinds and binds
7
+ * every listener again, and a truthy one binds the resize listeners, which runs the resize step.
8
+ */
9
+ const EFFECTS = {
10
+ responsive: {
11
+ fact: { file: 'dist/chart.js', pattern: '!!this._responsiveListeners !== options.responsive' },
12
+ effect:
13
+ 'Chart.js compares responsive with !== to whether it listens for resizes (a boolean), so any other value makes every update unbind and bind all its event listeners again; a truthy one also runs its resize step each time, which can recurse when the container size follows the chart.'
14
+ }
15
+ };
16
+
17
+ /**
18
+ * The chart options of every configuration (`config.options`) with the effect of a mismatched value
19
+ * on each option `EFFECTS` names, its runtime line added to the option's provenance.
20
+ */
21
+ export function withMismatchEffects(root, definitions, configNames) {
22
+ const next = { ...definitions };
23
+ const optionsNames = new Set(configNames.map((name) => definitions[name].properties.options.node.ref));
24
+ for (const name of optionsNames) {
25
+ const properties = { ...next[name].properties };
26
+ for (const [key, { fact, effect }] of Object.entries(EFFECTS)) {
27
+ const property = properties[key];
28
+ if (property === undefined) throw new Error(`chart.js types changed: ${name} has no option "${key}"`);
29
+ const source = { source: 'chartjs', pointer: runtimePointer(root, 'chart.js', fact) };
30
+ const provenance = { ...property.provenance, sources: [...property.provenance.sources, source] };
31
+ properties[key] = { ...property, provenance, onMismatch: effect };
32
+ }
33
+ next[name] = { ...next[name], properties };
34
+ }
35
+ return next;
36
+ }
@@ -0,0 +1,41 @@
1
+ import { runtimePointer } from './runtime.mjs';
2
+
3
+ /**
4
+ * Built-in plugin options (`options.plugins.legend`, …) take effect only when the plugin is
5
+ * registered: `createDescriptors` (dist/chart.js) reads `options[plugin.id]` for registered plugins
6
+ * only, so the options of a missing one are silently ignored. Each namespace must be the `id` of a
7
+ * plugin in the pinned runtime, or generation fails.
8
+ */
9
+ const DESCRIPTORS = { file: 'dist/chart.js', pattern: 'const opts = getOpts(options[id], all);' };
10
+ /** `getOpts` turns `false` into `null` and passes `null` through; `createDescriptors` then skips the plugin. */
11
+ const SKIPPED = { file: 'dist/chart.js', pattern: 'if (opts === null) {' };
12
+
13
+ const pluginPointer = (root, id) =>
14
+ runtimePointer(root, 'chart.js', { file: 'dist/chart.js', pattern: `id: '${id}',` });
15
+
16
+ /**
17
+ * The `options.plugins` definitions of the given configurations. `PluginOptionsByType` is the
18
+ * declaration-merging point of plugin options (dist/types/index.d.ts), so each is the
19
+ * `chartjs.plugins` extension point (CG-02-05): a plugin's options are added there by name.
20
+ */
21
+ export function pluginDefinitions(definitions, configNames) {
22
+ const optionsNames = configNames.map((name) => definitions[name].properties.options.node.ref);
23
+ return [...new Set(optionsNames.map((name) => definitions[name].properties.plugins.node.ref))];
24
+ }
25
+
26
+ /**
27
+ * Every built-in plugin namespace under `options.plugins` of the given configurations, with the
28
+ * handler to set and the runtime lines that make it a plugin id. The generator applies the plan.
29
+ */
30
+ export function planPluginHandlers(root, definitions, configNames) {
31
+ const descriptors = runtimePointer(root, 'chart.js', DESCRIPTORS);
32
+ const skipped = runtimePointer(root, 'chart.js', SKIPPED);
33
+ return pluginDefinitions(definitions, configNames).flatMap((definition) =>
34
+ Object.keys(definitions[definition].properties).map((property) => ({
35
+ definition,
36
+ property,
37
+ handledBy: { id: 'chartjs.plugins', name: property, enforced: false },
38
+ sources: [descriptors, skipped, pluginPointer(root, property)].map((pointer) => ({ source: 'chartjs', pointer }))
39
+ }))
40
+ );
41
+ }
@@ -0,0 +1,96 @@
1
+ import { compareText, resolveIn, sameNode, upstreamChanged } from './nodes.mjs';
2
+ import { runtimePointer } from './runtime.mjs';
3
+
4
+ /**
5
+ * Chart.js resolves element options through a scope chain that ends at the options root, and
6
+ * with element and hover prefixes (dist/chart.js `datasetElementScopeKeys`, `_resolveElementOptions`),
7
+ * so `options.radius`, `options.pointBackgroundColor` and `datasets[].radius` all reach the
8
+ * point elements. Per-property animations live under `options.animation`, which `animations`
9
+ * falls back to. The declarations describe none of this; `planOptionScopes` works out what to
10
+ * add, citing the runtime, and the generator applies the plan to the definitions it owns.
11
+ */
12
+ export const ELEMENT_SCOPE = 'ElementOptionsInScope';
13
+ const ELEMENTS = ['point', 'line', 'bar', 'arc'];
14
+
15
+ const RUNTIME = {
16
+ scopes: { file: 'dist/chart.js', pattern: 'datasetElementScopeKeys(datasetType, elementType) {' },
17
+ prefixes: { file: 'dist/chart.js', pattern: 'const prefixes = active ? [' },
18
+ animations: { file: 'dist/chunks/helpers.dataset.js', pattern: "_fallback: 'animation'" }
19
+ };
20
+
21
+ const changed = upstreamChanged('chart.js');
22
+
23
+ const capitalize = (name) => `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
24
+
25
+ /** `radius` → `pointRadius`; `hoverRadius` → `pointHoverRadius`, the `<element>Hover` prefix. */
26
+ function prefixed(element, name) {
27
+ return name.startsWith('hover') && name.length > 5
28
+ ? `${element}Hover${name.slice(5)}`
29
+ : `${element}${capitalize(name)}`;
30
+ }
31
+
32
+ /** Records one declared type for a name; a name shared by several elements collects each distinct type. */
33
+ function collect(collected, key, property, sources) {
34
+ const entry = collected.get(key) ?? { nodes: [], sources: [...property.provenance.sources, ...sources] };
35
+ if (!entry.nodes.some((node) => sameNode(node, property.node))) entry.nodes.push(property.node);
36
+ collected.set(key, entry);
37
+ }
38
+
39
+ /** Every name an element option is read under (`n` and `<element>N`), with its declared types. */
40
+ function elementOptionNames(elements, resolve, sources) {
41
+ const collected = new Map();
42
+ for (const element of ELEMENTS) {
43
+ const declared = resolve(elements.properties[element]?.node ?? { kind: 'unknown' });
44
+ if (declared.kind !== 'object') throw changed(`no declared options for the ${element} element`);
45
+ for (const [name, property] of Object.entries(declared.properties)) {
46
+ collect(collected, name, property, sources);
47
+ collect(collected, prefixed(element, name), property, sources);
48
+ }
49
+ }
50
+ return collected;
51
+ }
52
+
53
+ function scopeDefinition(elements, resolve, sources) {
54
+ const collected = elementOptionNames(elements, resolve, sources);
55
+ const properties = [...collected.keys()].sort(compareText).map((key) => {
56
+ const { nodes, sources: cited } = collected.get(key);
57
+ const node = nodes.length === 1 ? nodes[0] : { kind: 'union', anyOf: nodes };
58
+ return [key, { node, provenance: { confidence: 'authoritative', sources: cited } }];
59
+ });
60
+ return { kind: 'object', additional: 'unknown', properties: Object.fromEntries(properties) };
61
+ }
62
+
63
+ /** Dataset variants of a configuration, reached through `data.datasets[]`. */
64
+ function datasetNames(config, resolve) {
65
+ const datasets = resolve(resolve(config.properties.data.node).properties.datasets.node);
66
+ return resolve(datasets.items).anyOf.map((member) => member.ref);
67
+ }
68
+
69
+ /**
70
+ * What to add for the option scopes of the given configurations: the shared element-options
71
+ * definition, the definitions that include it (every `options` and every dataset variant), and the
72
+ * animation spec definitions that become open.
73
+ */
74
+ export function planOptionScopes(root, definitions, configNames) {
75
+ const resolve = resolveIn(definitions);
76
+ const sources = [RUNTIME.scopes, RUNTIME.prefixes].map((fact) => ({
77
+ source: 'chartjs',
78
+ pointer: runtimePointer(root, 'chart.js', fact)
79
+ }));
80
+ const configs = configNames.map((name) => definitions[name]);
81
+ const optionsNames = configs.map((config) => config.properties.options.node.ref);
82
+ const animationNames = optionsNames.flatMap((name) =>
83
+ (resolve(definitions[name].properties.animation.node).anyOf ?? [])
84
+ .filter((member) => resolve(member).kind === 'object')
85
+ .map((member) => member.ref)
86
+ );
87
+ return {
88
+ scope: scopeDefinition(resolve(definitions[optionsNames[0]].properties.elements.node), resolve, sources),
89
+ includedBy: [...new Set([...optionsNames, ...configs.flatMap((config) => datasetNames(config, resolve))])],
90
+ openAnimations: [...new Set(animationNames)],
91
+ animationProvenance: {
92
+ confidence: 'authoritative',
93
+ sources: [{ source: 'chartjs', pointer: runtimePointer(root, 'chart.js', RUNTIME.animations) }]
94
+ }
95
+ };
96
+ }
@@ -0,0 +1,275 @@
1
+ import { withMismatchEffects } from './chartjs-effects.mjs';
2
+ import { planPluginHandlers, pluginDefinitions } from './chartjs-plugins.mjs';
3
+ import { ELEMENT_SCOPE, planOptionScopes } from './chartjs-scopes.mjs';
4
+ import { TypeEmitter } from './emitter.mjs';
5
+ import { createEntryProgram, exportedAliasTypes } from './program.mjs';
6
+ import { runtimeProvenance } from './runtime.mjs';
7
+ import { makeOptionsScriptable } from './scriptable.mjs';
8
+ import { artifactOf, typeLookups, unionMembers } from './types.mjs';
9
+
10
+ const SOURCE_ID = 'chartjs';
11
+ /**
12
+ * The runtime registries (`Chart.register`) the `type` values are looked up in: every dataset's
13
+ * controller (`dataset.type || config.type`) and every configured scale. The chart `type` itself is
14
+ * read only through its datasets: with none, an unregistered chart type renders, so it is not enforced.
15
+ */
16
+ const LOOKUPS = {
17
+ controllers: { file: 'dist/chart.js', pattern: 'const ControllerClass = registry.getController(type);' },
18
+ scales: { file: 'dist/chart.js', pattern: 'const scaleClass = registry.getScale(scaleType);' }
19
+ };
20
+
21
+ /**
22
+ * Scales take plugin variants (`withExtensions`, CG-02-05) at their registry's name; controllers at two
23
+ * points of one registry, `chartjs.charts` and `chartjs.datasets`, since a controller adds a chart type
24
+ * and a dataset type at once (part 2, marked where the unions are built).
25
+ */
26
+ function registryFacts(root) {
27
+ const provenance = (fact) => runtimeProvenance(root, { sourceId: SOURCE_ID, packageName: 'chart.js' }, fact);
28
+ const fact = (id, enforced, lookup) => ({ registry: { id, prefix: '', enforced }, provenance: provenance(lookup) });
29
+ return {
30
+ chartType: fact('chartjs.controllers', false, LOOKUPS.controllers),
31
+ dataset: fact('chartjs.controllers', true, LOOKUPS.controllers),
32
+ scale: { ...fact('chartjs.scales', true, LOOKUPS.scales), extensionPoint: 'chartjs.scales' }
33
+ };
34
+ }
35
+
36
+ /** `DeepPartial<T>` / `_DeepPartialObject<T>` (dist/types/utils.d.ts) map over primitives unchanged. */
37
+ const WRAPPERS = new Set(['DeepPartial', '_DeepPartialObject']);
38
+
39
+ /** Chart types as declared by the augmentable `ChartTypeRegistry`. */
40
+ function registeredChartTypes(root) {
41
+ const { program, checker, entry } = createEntryProgram(
42
+ root,
43
+ "import type { ChartTypeRegistry } from 'chart.js';\nexport type Registry = keyof ChartTypeRegistry;"
44
+ );
45
+ const registry = exportedAliasTypes(checker, entry).get('Registry');
46
+ return {
47
+ program,
48
+ chartTypes: unionMembers(registry)
49
+ .map((member) => member.value)
50
+ .sort()
51
+ };
52
+ }
53
+
54
+ function entryText(chartTypes) {
55
+ const lines = chartTypes.flatMap((chartType) => [
56
+ `export type Config_${chartType} = ChartConfiguration<'${chartType}'>;`,
57
+ `export type Dataset_${chartType} = ChartDataset<'${chartType}'>;`
58
+ ]);
59
+ return [
60
+ "import type { ChartConfiguration, ChartDataset, ChartOptions } from 'chart.js';",
61
+ // Options of every chart type at once: their `scales` take every scale type.
62
+ 'export type AnyOptions = ChartOptions;',
63
+ ...lines
64
+ ].join('\n');
65
+ }
66
+
67
+ /**
68
+ * Positions where the declarations are narrower or wider than the runtime contract:
69
+ * data points depend on the caller's generic `TData`, inline plugins are user objects,
70
+ * `options.plugins` is extended by third-party plugins through declaration merging,
71
+ * `options.scales[*]` variants come from `ScaleTypeRegistry`, which plugins merge into
72
+ * (chartjs-plugin-streaming, chartjs-chart-matrix), and `config.data`, `data.datasets`
73
+ * and `dataset.data` are defaulted by the runtime (`initConfig`/`initData` in
74
+ * chart.js/dist/chart.js, `config.data = config.data || {}`, `data.datasets = data.datasets || []`,
75
+ * `_dataCheck`: `dataset.data || (dataset.data = [])`), so their absence is not an error.
76
+ */
77
+ function adjustChartType(lookups, { config, datasetTypes }, into) {
78
+ const { property, propertyType, indexValueType, elementType } = lookups;
79
+ const data = propertyType(config, 'data');
80
+ const datasetType = elementType(propertyType(data, 'datasets'));
81
+ const options = propertyType(config, 'options');
82
+ into.opaque.add(elementType(propertyType(config, 'plugins')));
83
+ const plugins = propertyType(options, 'plugins');
84
+ into.open.add(plugins);
85
+ // getOpts (chart.js `PluginService`): `false` and `null` disable a plugin (`createDescriptors` skips
86
+ // it), `true` enables it with defaults; each plugin entry cites both lines (chartjs-plugins.mjs).
87
+ for (const plugin of lookups.checker.getPropertiesOfType(plugins))
88
+ into.widened.set(declarationOf(plugin), [{ kind: 'boolean' }, { kind: 'null' }]);
89
+ into.scales.add(indexValueType(propertyType(options, 'scales')));
90
+ into.runtimeOptional.add(property(config, 'data'));
91
+ into.runtimeOptional.add(property(data, 'datasets'));
92
+ for (const dataset of [datasetType, ...datasetTypes]) {
93
+ into.opaque.add(propertyType(dataset, 'data'));
94
+ into.runtimeOptional.add(property(dataset, 'data'));
95
+ }
96
+ into.datasetVariants.set(datasetType, [datasetType, ...datasetTypes]);
97
+ }
98
+
99
+ /**
100
+ * The declaration a plugin's option is widened at. `widened` is keyed by declaration: a symbol without
101
+ * one (a key a mapped type synthesizes) would key `undefined`, which every other declaration-less
102
+ * property also looks up, so it would widen them all.
103
+ */
104
+ export function declarationOf(symbol) {
105
+ const [declaration] = symbol.declarations ?? [];
106
+ if (declaration === undefined)
107
+ throw new Error(`chart.js types changed: plugin option "${symbol.name}" has no declaration`);
108
+ return declaration;
109
+ }
110
+
111
+ /**
112
+ * Every chart type takes every scale type: the runtime looks a scale's type up in one registry,
113
+ * whatever the chart type (`registry.getScale(scaleType)`), and chooses an untyped scale's type by
114
+ * its axis (`radialLinear` on `r`, `category` or `linear` on `x` and `y`) in any chart. The
115
+ * declarations narrow `scales` by chart type (`ChartTypeRegistry[TType]['scales']`): a radar chart's
116
+ * `x` scale, which the runtime builds as `category`, would be checked as `radialLinear`. Each chart
117
+ * type's scale type is emitted as the scale type of `ChartOptions` over every chart type.
118
+ */
119
+ function everyScaleType(lookups, anyOptions, into) {
120
+ const every = lookups.indexValueType(lookups.propertyType(lookups.checker.getNonNullableType(anyOptions), 'scales'));
121
+ return { every, own: [...into.scales].filter((own) => own !== every) };
122
+ }
123
+
124
+ /** Each chart type's scale type emitted as the one of every chart type. */
125
+ const scaleOverrides = (emitter, { every, own }) => own.map((type) => [type, () => emitter.emit(every)]);
126
+
127
+ function contractAdjustments(checker, chartTypes, aliases, facts) {
128
+ const into = {
129
+ opaque: new Set(),
130
+ open: new Set(),
131
+ registries: new Map(),
132
+ runtimeOptional: new Set(),
133
+ widened: new Map(),
134
+ datasetVariants: new Map(),
135
+ scales: new Set()
136
+ };
137
+ for (const chartType of chartTypes) {
138
+ const config = aliases.get(`Config_${chartType}`);
139
+ const datasetTypes = chartTypes
140
+ .filter((other) => other !== chartType)
141
+ .map((other) => aliases.get(`Dataset_${other}`));
142
+ adjustChartType(typeLookups(checker, 'chart.js'), { config, datasetTypes }, into);
143
+ }
144
+ const scales = everyScaleType(typeLookups(checker, 'chart.js'), aliases.get('AnyOptions'), into);
145
+ into.registries.set(scales.every, facts.scale);
146
+ return { ...into, scales };
147
+ }
148
+
149
+ /** Mixed charts: a dataset without `type` inherits the chart type; plugins may register more types. */
150
+ function datasetUnions(emitter, datasetVariants, { registry, provenance }) {
151
+ return new Map(
152
+ [...datasetVariants].map(([datasetType, variants]) => [
153
+ datasetType,
154
+ () => ({
155
+ kind: 'union',
156
+ anyOf: variants.map((variant) => emitter.reference(variant)),
157
+ discriminator: 'type',
158
+ defaultVariant: 0,
159
+ extensible: true,
160
+ registry,
161
+ extensionPoint: 'chartjs.datasets',
162
+ provenance
163
+ })
164
+ ])
165
+ );
166
+ }
167
+
168
+ /**
169
+ * `toFont` (dist/chunks/helpers.dataset.js) reads a font's `size` with `parseInt` when it is a string,
170
+ * and tests `style` against its own pattern (`unset`, `oblique 10deg` are styles too): a string is
171
+ * a size, and any string a style the runtime decides itself (A6: runtime-handled values are accepted).
172
+ */
173
+ const FONT_FACTS = {
174
+ size: { file: 'dist/chunks/helpers.dataset.js', pattern: 'size = parseInt(size, 10);' },
175
+ style: { file: 'dist/chunks/helpers.dataset.js', pattern: "if (style && !('' + style).match(FONT_STYLE)) {" }
176
+ };
177
+ const STRING = { kind: 'string' };
178
+
179
+ /** `node` also accepting a string; a scriptable value widens inside its callback-or-value union. */
180
+ function withString(node) {
181
+ if (node.kind === 'scriptable') return { ...node, value: withString(node.value) };
182
+ if (node.kind === 'union') return { ...node, anyOf: [...node.anyOf, STRING] };
183
+ return { kind: 'union', anyOf: [node, STRING] };
184
+ }
185
+
186
+ /** Every font definition (its own `size` and `family`) with the facts of `toFont`, each citing its line. */
187
+ function withFontFacts(root, definitions) {
188
+ const cite = (fact) => ({
189
+ source: SOURCE_ID,
190
+ pointer: runtimeProvenance(root, { sourceId: SOURCE_ID, packageName: 'chart.js' }, fact).sources[0].pointer
191
+ });
192
+ const next = { ...definitions };
193
+ for (const [name, definition] of Object.entries(definitions)) {
194
+ const properties = definition.properties ?? {};
195
+ if (properties.size === undefined || properties.family === undefined) continue;
196
+ const widened = Object.fromEntries(
197
+ Object.entries(FONT_FACTS)
198
+ .filter(([key]) => properties[key] !== undefined)
199
+ .map(([key, fact]) => {
200
+ const property = properties[key];
201
+ const provenance = { ...property.provenance, sources: [...property.provenance.sources, cite(fact)] };
202
+ return [key, { ...property, node: withString(property.node), provenance }];
203
+ })
204
+ );
205
+ next[name] = { ...definition, properties: { ...properties, ...widened } };
206
+ }
207
+ return next;
208
+ }
209
+
210
+ /**
211
+ * What the declarations leave out, read from the runtime: element options in every scope the
212
+ * runtime reads them from, open per-property animations, built-in plugin options that take effect
213
+ * only when the plugin is registered, the plugin extension point; then every option value becomes
214
+ * scriptable, fonts take what `toFont` handles, and options the runtime mishandles say how (A73).
215
+ */
216
+ function withRuntimeFacts(root, definitions, configNames) {
217
+ const scopes = planOptionScopes(root, definitions, configNames);
218
+ const next = { ...definitions, [ELEMENT_SCOPE]: scopes.scope };
219
+ for (const name of scopes.includedBy) next[name] = { ...next[name], includes: [ELEMENT_SCOPE] };
220
+ for (const name of scopes.openAnimations)
221
+ next[name] = { ...next[name], additional: 'open', provenance: scopes.animationProvenance };
222
+ for (const { definition, property, handledBy, sources } of planPluginHandlers(root, definitions, configNames)) {
223
+ const target = next[definition].properties[property];
224
+ const provenance = { ...target.provenance, sources: [...target.provenance.sources, ...sources] };
225
+ next[definition] = {
226
+ ...next[definition],
227
+ properties: { ...next[definition].properties, [property]: { ...target, handledBy, provenance } }
228
+ };
229
+ }
230
+ for (const name of pluginDefinitions(definitions, configNames))
231
+ next[name] = { ...next[name], extensionPoint: 'chartjs.plugins' };
232
+ makeOptionsScriptable(next, configNames);
233
+ return withMismatchEffects(root, withFontFacts(root, next), configNames);
234
+ }
235
+
236
+ export function generateChartjs(root, pin) {
237
+ const { program: registryProgram, chartTypes } = registeredChartTypes(root);
238
+ const { program, checker, entry } = createEntryProgram(root, entryText(chartTypes), registryProgram);
239
+ const aliases = exportedAliasTypes(checker, entry);
240
+ const facts = registryFacts(root);
241
+ const { datasetVariants, scales, ...adjustments } = contractAdjustments(checker, chartTypes, aliases, facts);
242
+ const emitter = new TypeEmitter({
243
+ program,
244
+ checker,
245
+ sourceId: SOURCE_ID,
246
+ packageName: pin.package,
247
+ wrappers: WRAPPERS,
248
+ ...adjustments
249
+ });
250
+ emitter.overrides = new Map([
251
+ ...datasetUnions(emitter, datasetVariants, facts.dataset),
252
+ ...scaleOverrides(emitter, scales)
253
+ ]);
254
+ const configs = chartTypes.map((chartType) => emitter.reference(aliases.get(`Config_${chartType}`)));
255
+ const definitions = withRuntimeFacts(
256
+ root,
257
+ emitter.definitions,
258
+ configs.map(({ ref }) => ref)
259
+ );
260
+ return artifactOf({
261
+ library: 'chartjs',
262
+ sourceId: SOURCE_ID,
263
+ pin,
264
+ definitions,
265
+ // A controller adds a chart type and a dataset type: two variant points of one registry (CG-02-05 part 2).
266
+ root: {
267
+ kind: 'union',
268
+ anyOf: configs,
269
+ discriminator: 'type',
270
+ extensible: true,
271
+ ...facts.chartType,
272
+ extensionPoint: 'chartjs.charts'
273
+ }
274
+ });
275
+ }
@@ -0,0 +1,21 @@
1
+ import ts from 'typescript';
2
+
3
+ /** "Use `x` instead", "use x instead": what a deprecation notice says to use (an option name or path). */
4
+ const REPLACEMENT = /\buse\s+`?([A-Za-z_$][\w$.]*)`?\s+instead\b/i;
5
+
6
+ /**
7
+ * The `@deprecated` tag of a property, as the upstream declaration states it: its first sentence,
8
+ * and the replacement it names. Never a rename: a notice does not say the runtime reads the old
9
+ * option under the new name, so renaming could change what is drawn.
10
+ */
11
+ export function deprecationOf(symbol) {
12
+ const tag = symbol.getJsDocTags().find(({ name }) => name === 'deprecated');
13
+ if (tag === undefined) return undefined;
14
+ const text = ts.displayPartsToString(tag.text).replace(/\s+/g, ' ').trim();
15
+ const [sentence] = text.split(/(?<=\.)\s/);
16
+ const replacement = REPLACEMENT.exec(text)?.[1];
17
+ return {
18
+ ...(sentence === undefined || sentence === '' ? {} : { message: sentence }),
19
+ ...(replacement === undefined ? {} : { replacement })
20
+ };
21
+ }
package/lib/digest.mjs ADDED
@@ -0,0 +1,33 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /** The committed artifact text: tab-indented JSON with a final newline. */
4
+ export function serialize(artifact) {
5
+ return `${JSON.stringify(artifact, null, '\t')}\n`;
6
+ }
7
+
8
+ /** The object without the named keys, key order kept. */
9
+ export function omit(object, keys) {
10
+ return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
11
+ }
12
+
13
+ /** `sha256-<hex>` of the canonical text with the `digest` field left out. */
14
+ export function digestOf(artifact) {
15
+ return `sha256-${createHash('sha256')
16
+ .update(serialize(omit(artifact, ['digest'])))
17
+ .digest('hex')}`;
18
+ }
19
+
20
+ /** The artifact with its digest placed in the header, right after `generator`. */
21
+ export function withDigest(artifact) {
22
+ const digest = digestOf(artifact);
23
+ return Object.fromEntries(
24
+ Object.entries(artifact).flatMap(([key, value]) =>
25
+ key === 'generator'
26
+ ? [
27
+ [key, value],
28
+ ['digest', digest]
29
+ ]
30
+ : [[key, value]]
31
+ )
32
+ );
33
+ }