@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
|
@@ -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
|
+
}
|
package/lib/chartjs.mjs
ADDED
|
@@ -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
|
+
}
|