@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.
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/index.d.mts +6 -0
- package/dist/lib/chartjs-effects.d.mts +5 -0
- package/dist/lib/chartjs-plugins.d.mts +23 -0
- package/dist/lib/chartjs-scopes.d.mts +30 -0
- package/dist/lib/chartjs.d.mts +32 -0
- package/dist/lib/deprecation.d.mts +9 -0
- package/dist/lib/digest.d.mts +12 -0
- package/dist/lib/echarts-components.d.mts +2 -0
- package/dist/lib/echarts-deprecations.d.mts +5 -0
- package/dist/lib/echarts-handlers.d.mts +2 -0
- package/dist/lib/echarts-runtime.d.mts +2 -0
- package/dist/lib/echarts.d.mts +26 -0
- package/dist/lib/emitter.d.mts +173 -0
- package/dist/lib/generate.d.mts +14 -0
- package/dist/lib/highcharts-runtime.d.mts +2 -0
- package/dist/lib/highcharts.d.mts +26 -0
- package/dist/lib/local.d.mts +14 -0
- package/dist/lib/lock.d.mts +31 -0
- package/dist/lib/nodes.d.mts +4 -0
- package/dist/lib/paths.d.mts +22 -0
- package/dist/lib/program.d.mts +13 -0
- package/dist/lib/provenance.d.mts +46 -0
- package/dist/lib/runtime.d.mts +24 -0
- package/dist/lib/scriptable.d.mts +13 -0
- package/dist/lib/trace.d.mts +8 -0
- package/dist/lib/types.d.mts +52 -0
- package/index.mjs +20 -0
- package/lib/chartjs-effects.mjs +36 -0
- package/lib/chartjs-plugins.mjs +41 -0
- package/lib/chartjs-scopes.mjs +96 -0
- package/lib/chartjs.mjs +275 -0
- package/lib/deprecation.mjs +21 -0
- package/lib/digest.mjs +33 -0
- package/lib/echarts-components.mjs +176 -0
- package/lib/echarts-deprecations.mjs +206 -0
- package/lib/echarts-handlers.mjs +31 -0
- package/lib/echarts-runtime.mjs +311 -0
- package/lib/echarts.mjs +89 -0
- package/lib/emitter.mjs +333 -0
- package/lib/generate.mjs +35 -0
- package/lib/highcharts-runtime.mjs +198 -0
- package/lib/highcharts.mjs +105 -0
- package/lib/local.mjs +33 -0
- package/lib/lock.mjs +123 -0
- package/lib/nodes.mjs +17 -0
- package/lib/paths.mjs +50 -0
- package/lib/program.mjs +46 -0
- package/lib/provenance.mjs +100 -0
- package/lib/runtime.mjs +38 -0
- package/lib/scriptable.mjs +57 -0
- package/lib/trace.mjs +124 -0
- package/lib/types.mjs +74 -0
- package/package.json +47 -5
- package/upstream-sources.json +102 -0
package/lib/local.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { generateArtifact, isRedistributable } from './generate.mjs';
|
|
5
|
+
import { installedUpstream, pinOf, UpstreamError } from './lock.mjs';
|
|
6
|
+
import { localSchemaPath } from './paths.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds the schema of `library` from the project's own install and writes it next to it. Only for
|
|
10
|
+
* libraries whose schema cannot ship with their adapter; the generator must find every declaration
|
|
11
|
+
* shape and runtime fact it relies on in that install, or nothing is written.
|
|
12
|
+
* @param {string} root
|
|
13
|
+
* @param {string} library
|
|
14
|
+
* @returns {{ path: string, version: string, digest: string, integrity: string }}
|
|
15
|
+
*/
|
|
16
|
+
export function buildLocalSchema(root, library) {
|
|
17
|
+
if (isRedistributable(pinOf(library)))
|
|
18
|
+
throw new UpstreamError(`${library} ships its schema with its adapter; there is nothing to build.`);
|
|
19
|
+
const pin = installedUpstream(root, library);
|
|
20
|
+
let generated;
|
|
21
|
+
try {
|
|
22
|
+
generated = generateArtifact(root, library, pin);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new UpstreamError(
|
|
26
|
+
`${pin.package} ${pin.version} could not be read by this generator (it was verified against ${pinOf(library).version}): ${reason}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const path = localSchemaPath(root, library, pin.version);
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
writeFileSync(path, generated.text);
|
|
32
|
+
return { path, version: pin.version, digest: generated.artifact.digest, integrity: pin.integrity };
|
|
33
|
+
}
|
package/lib/lock.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/** The upstream pins this generator was written and verified against (one exact version per library). */
|
|
7
|
+
const PINS = JSON.parse(readFileSync(new URL('../upstream-sources.json', import.meta.url), 'utf8'));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The project's install cannot be used: not installed, another major than the generator supports,
|
|
11
|
+
* a library whose schema ships with its adapter, or declarations and runtime the generator cannot
|
|
12
|
+
* read. A situation for the user to resolve, not a defect of the generator.
|
|
13
|
+
*/
|
|
14
|
+
export class UpstreamError extends Error {
|
|
15
|
+
name = 'UpstreamError';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The upstream pin for `library` from upstream-sources.json; throws `UpstreamError` when there is none. */
|
|
19
|
+
export function pinOf(library) {
|
|
20
|
+
const pin = PINS.libraries.find((entry) => entry.library === library);
|
|
21
|
+
if (pin === undefined) throw new UpstreamError(`No upstream pin for ${library}`);
|
|
22
|
+
return pin;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The TypeScript this generator was verified with. Its lib files decide the members of the DOM and
|
|
27
|
+
* built-in types the declarations name, so another version builds another artifact (CG-07-06).
|
|
28
|
+
*/
|
|
29
|
+
export function compilerPin() {
|
|
30
|
+
return PINS.compiler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Throws `UpstreamError` unless `version` (the TypeScript the generator loaded) is the pinned compiler. */
|
|
34
|
+
export function verifiedCompiler(version) {
|
|
35
|
+
const pin = compilerPin();
|
|
36
|
+
if (version !== pin.version)
|
|
37
|
+
throw new UpstreamError(
|
|
38
|
+
`typescript ${version} is loaded, but this generator builds schemas only with typescript ${pin.version}: another version reads the declarations differently.`
|
|
39
|
+
);
|
|
40
|
+
return pin;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The directory of a package an artifact names as a source: the pinned compiler is the TypeScript
|
|
45
|
+
* this generator loads (its own exact dependency); a library is the project's install under `root`.
|
|
46
|
+
*/
|
|
47
|
+
export function sourceDirectory(root, packageName) {
|
|
48
|
+
if (packageName === compilerPin().package)
|
|
49
|
+
return dirname(createRequire(import.meta.url).resolve(`${packageName}/package.json`));
|
|
50
|
+
return join(root, 'node_modules', packageName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function installedVersion(root, packageName) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(join(root, 'node_modules', packageName, 'package.json'), 'utf8')).version;
|
|
56
|
+
} catch {
|
|
57
|
+
throw new UpstreamError(`${packageName} is not installed in ${root}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function lockfileEntry(root, packageName) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(readFileSync(join(root, 'package-lock.json'), 'utf8')).packages?.[`node_modules/${packageName}`];
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Upstream pin from upstream-sources.json, verified against what npm installed in this repository. */
|
|
70
|
+
export function verifiedUpstream(root, library) {
|
|
71
|
+
const pin = pinOf(library);
|
|
72
|
+
const installed = installedVersion(root, pin.package);
|
|
73
|
+
const locked = lockfileEntry(root, pin.package);
|
|
74
|
+
if (installed !== pin.version || locked?.version !== pin.version)
|
|
75
|
+
throw new UpstreamError(`${pin.package}: installed ${installed}, pinned ${pin.version}`);
|
|
76
|
+
if (locked.integrity !== pin.integrity)
|
|
77
|
+
throw new UpstreamError(`${pin.package}: lockfile integrity differs from the pin`);
|
|
78
|
+
return pin;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function packageFiles(directory, root = directory) {
|
|
82
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
83
|
+
.flatMap((entry) => {
|
|
84
|
+
const path = join(directory, entry.name);
|
|
85
|
+
if (entry.isDirectory()) return entry.name === 'node_modules' ? [] : packageFiles(path, root);
|
|
86
|
+
return entry.isFile() ? [relative(root, path).split(sep).join('/')] : [];
|
|
87
|
+
})
|
|
88
|
+
.sort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `sha256-<base64>` over every file of an installed package, path and content: what the schema was derived from. */
|
|
92
|
+
export function contentIntegrity(directory) {
|
|
93
|
+
const hash = createHash('sha256');
|
|
94
|
+
for (const file of packageFiles(directory))
|
|
95
|
+
hash
|
|
96
|
+
.update(file)
|
|
97
|
+
.update('\0')
|
|
98
|
+
.update(readFileSync(join(directory, file)))
|
|
99
|
+
.update('\0');
|
|
100
|
+
return `sha256-${hash.digest('base64')}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const major = (version) => version.split('.')[0];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The pin for the version a project actually installed: the same major as this generator's pin,
|
|
107
|
+
* with the integrity of that install (its npm lockfile entry, or a hash of its files), so the
|
|
108
|
+
* facts derived from it stay traceable to exactly what was read.
|
|
109
|
+
*/
|
|
110
|
+
export function installedUpstream(root, library) {
|
|
111
|
+
const pin = pinOf(library);
|
|
112
|
+
const version = installedVersion(root, pin.package);
|
|
113
|
+
if (major(version) !== major(pin.version))
|
|
114
|
+
throw new UpstreamError(
|
|
115
|
+
`${pin.package} ${version} is not supported: this generator reads ${major(pin.version)}.x (pinned ${pin.version}).`
|
|
116
|
+
);
|
|
117
|
+
const locked = lockfileEntry(root, pin.package);
|
|
118
|
+
const integrity =
|
|
119
|
+
locked?.version === version && typeof locked.integrity === 'string'
|
|
120
|
+
? locked.integrity
|
|
121
|
+
: contentIntegrity(join(root, 'node_modules', pin.package));
|
|
122
|
+
return { ...pin, version, integrity };
|
|
123
|
+
}
|
package/lib/nodes.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** IR helpers every generator module shares. */
|
|
2
|
+
|
|
3
|
+
/** `node`, or the definition it references. */
|
|
4
|
+
export const resolveIn = (definitions) => (node) => (node.kind === 'ref' ? definitions[node.ref] : node);
|
|
5
|
+
|
|
6
|
+
/** Two IR nodes are the same when they serialize the same (generated nodes keep a fixed key order). */
|
|
7
|
+
export const sameNode = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
8
|
+
|
|
9
|
+
/** Code-point order: `localeCompare` depends on the machine's locale, generated output must not. */
|
|
10
|
+
export const compareText = (left, right) => (left < right ? -1 : Number(left > right));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The error generation fails with when the upstream no longer has the shape a generator reads
|
|
14
|
+
* (`upstreamChanged('echarts types')('no "series" property')`): an upstream change fails loudly, never
|
|
15
|
+
* into a silently different artifact.
|
|
16
|
+
*/
|
|
17
|
+
export const upstreamChanged = (subject) => (what) => new Error(`${subject} changed: ${what}`);
|
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
// Imported, never read from beside this file: bundles (the VS Code extension's server) hold this module without them.
|
|
4
|
+
import GENERATOR from '../package.json' with { type: 'json' };
|
|
5
|
+
import PINS from '../upstream-sources.json' with { type: 'json' };
|
|
6
|
+
|
|
7
|
+
const COMPILER = PINS.compiler;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where schemas that may not be redistributed (Highcharts) are written: under the project's own
|
|
11
|
+
* node_modules, so never tracked by git and never inside a package's `files`. A module of its own,
|
|
12
|
+
* so tools that only look a schema up (the CLI) do not load the generator and the TypeScript compiler.
|
|
13
|
+
*/
|
|
14
|
+
export const LOCAL_ARTIFACTS = 'node_modules/.cache/chartguard';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The schema built for `library` at `version` in the project at `root`.
|
|
18
|
+
* @param {string} root
|
|
19
|
+
* @param {string} library
|
|
20
|
+
* @param {string} version
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function localSchemaPath(root, library, version) {
|
|
24
|
+
return join(root, LOCAL_ARTIFACTS, `${library}-${version}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = (value) => (typeof value === 'string' ? value : undefined);
|
|
28
|
+
|
|
29
|
+
/** `@chartguard/schema-generator 0.1.0 with typescript@5.9.3`, as a schema's header records it. */
|
|
30
|
+
function builder(header) {
|
|
31
|
+
const generator = header.generator !== null && typeof header.generator === 'object' ? header.generator : {};
|
|
32
|
+
const sources = header.sources !== null && typeof header.sources === 'object' ? header.sources : {};
|
|
33
|
+
const compiler = sources.typescript !== null && typeof sources.typescript === 'object' ? sources.typescript : {};
|
|
34
|
+
const name = text(generator.name) ?? 'an unnamed generator';
|
|
35
|
+
const version = text(generator.version) ?? 'of no recorded version';
|
|
36
|
+
return `${name} ${version} with ${text(compiler.locator) ?? 'no recorded compiler'}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Why a schema built on the user's machine cannot be used by this ChartGuard, or `undefined` when its
|
|
41
|
+
* own generator built it with its own compiler: another generator or TypeScript reads the same install
|
|
42
|
+
* differently, so its schema is rebuilt, never used as if it were this one's (CG-07-06).
|
|
43
|
+
* @param {unknown} artifact the parsed schema file
|
|
44
|
+
* @returns {string | undefined}
|
|
45
|
+
*/
|
|
46
|
+
export function otherBuild(artifact) {
|
|
47
|
+
const built = builder(artifact !== null && typeof artifact === 'object' ? artifact : {});
|
|
48
|
+
const ours = `${GENERATOR.name} ${GENERATOR.version} with typescript@${COMPILER.version}`;
|
|
49
|
+
return built === ours ? undefined : `it was built by ${built}; this ChartGuard builds with ${ours}`;
|
|
50
|
+
}
|
package/lib/program.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
const COMPILER_OPTIONS = {
|
|
6
|
+
strict: true,
|
|
7
|
+
noEmit: true,
|
|
8
|
+
skipLibCheck: true,
|
|
9
|
+
target: ts.ScriptTarget.ES2022,
|
|
10
|
+
module: ts.ModuleKind.NodeNext,
|
|
11
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
12
|
+
lib: ['lib.es2022.d.ts', 'lib.dom.d.ts'],
|
|
13
|
+
types: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type-checks a generated in-memory entry file against installed declarations.
|
|
18
|
+
* Declarations are only parsed and type-checked; nothing is executed.
|
|
19
|
+
* A previous program for the same root lets TypeScript reuse its parsed declaration files.
|
|
20
|
+
*/
|
|
21
|
+
export function createEntryProgram(root, entryText, previous) {
|
|
22
|
+
const entryPath = resolve(root, '__chartguard_schema_entry__.ts');
|
|
23
|
+
const host = ts.createCompilerHost(COMPILER_OPTIONS);
|
|
24
|
+
const readSourceFile = host.getSourceFile.bind(host);
|
|
25
|
+
const fileExists = host.fileExists.bind(host);
|
|
26
|
+
// TypeScript hands the host its own spelling of a path (`D:/a/b.ts` for `D:\a\b.ts` on Windows):
|
|
27
|
+
// a name is the entry when it resolves to the same path, never by its text.
|
|
28
|
+
const isEntry = (fileName) => resolve(fileName) === entryPath;
|
|
29
|
+
host.getSourceFile = (fileName, languageVersion, ...rest) =>
|
|
30
|
+
isEntry(fileName)
|
|
31
|
+
? ts.createSourceFile(fileName, entryText, languageVersion, true)
|
|
32
|
+
: readSourceFile(fileName, languageVersion, ...rest);
|
|
33
|
+
host.fileExists = (fileName) => isEntry(fileName) || fileExists(fileName);
|
|
34
|
+
const program = ts.createProgram([entryPath], COMPILER_OPTIONS, host, previous);
|
|
35
|
+
const entry = program.getSourceFile(entryPath);
|
|
36
|
+
return { program, checker: program.getTypeChecker(), entry };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The type named by each `export type Name = ...` alias in the entry file. */
|
|
40
|
+
export function exportedAliasTypes(checker, entry) {
|
|
41
|
+
return new Map(
|
|
42
|
+
entry.statements
|
|
43
|
+
.filter((statement) => ts.isTypeAliasDeclaration(statement))
|
|
44
|
+
.map((statement) => [statement.name.text, checker.getTypeAtLocation(statement.name)])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
import { COMPILER_SOURCE } from './types.mjs';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_PATH = /^.*\/node_modules\//;
|
|
6
|
+
|
|
7
|
+
/** `chart.js/dist/types/index.d.ts` for a file under node_modules, else undefined. */
|
|
8
|
+
function packageRelative(fileName) {
|
|
9
|
+
return PACKAGE_PATH.test(fileName) ? fileName.replace(PACKAGE_PATH, '') : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `typescript/lib/lib.dom.d.ts:12`-style pointer: a package-relative file and the line a declaration starts on. */
|
|
13
|
+
function pointerOf(path, declaration) {
|
|
14
|
+
const file = declaration.getSourceFile();
|
|
15
|
+
const { line } = file.getLineAndCharacterOfPosition(declaration.getStart(file));
|
|
16
|
+
return `${path}:${String(line + 1)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Which published declaration a fact cites, and how that becomes a provenance record.
|
|
21
|
+
* Pointers are package-relative (`chart.js/dist/types/index.d.ts:12`); local paths never leak.
|
|
22
|
+
* Only declarations inside the pinned package count: TypeScript's own lib (`Pick`, `Partial`)
|
|
23
|
+
* and other installed packages are not evidence for the pinned source. A fact TypeScript's lib
|
|
24
|
+
* declares (the members of `Array` or of the DOM's event map) cites that lib, as `derived` (CG-07-06).
|
|
25
|
+
*/
|
|
26
|
+
export class Provenance {
|
|
27
|
+
constructor(checker, { sourceId, packageName }) {
|
|
28
|
+
this.checker = checker;
|
|
29
|
+
this.sourceId = sourceId;
|
|
30
|
+
this.prefix = `${packageName}/`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The pinned-package-relative path of a symbol's declaration, if it is declared there. */
|
|
34
|
+
pinnedPath(symbol) {
|
|
35
|
+
const fileName = symbol?.declarations?.[0]?.getSourceFile().fileName;
|
|
36
|
+
const relative = fileName === undefined ? undefined : packageRelative(fileName);
|
|
37
|
+
return relative?.startsWith(this.prefix) === true ? relative : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isPublished = (symbol) => this.pinnedPath(symbol) !== undefined;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The published symbol an object type should cite: its alias when published, else its own
|
|
44
|
+
* symbol, else what it is composed from, else the published type its (generator-local) alias names.
|
|
45
|
+
*/
|
|
46
|
+
ownerOf(type) {
|
|
47
|
+
if (this.isPublished(type.aliasSymbol)) return type.aliasSymbol;
|
|
48
|
+
const own = type.getSymbol();
|
|
49
|
+
if (this.isPublished(own)) return own;
|
|
50
|
+
return this.ownerOfComposite(type) ?? this.aliasTarget(type) ?? own;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `A & { … }` cites its first published member; a lib mapped type over a package type
|
|
55
|
+
* (`Pick<LabelOption, …>`, `Omit<…>`) cites that type. Each property still cites its own declaration.
|
|
56
|
+
*/
|
|
57
|
+
ownerOfComposite(type) {
|
|
58
|
+
const parts = type.isIntersection() ? type.types : (type.aliasTypeArguments ?? []).slice(0, 1);
|
|
59
|
+
return parts.map((part) => this.ownerOf(part)).find(this.isPublished);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `export type Dataset_bar = ChartDataset<'bar'>` in the entry file names `ChartDataset`. */
|
|
63
|
+
aliasTarget(type) {
|
|
64
|
+
const declaration = type.aliasSymbol?.declarations?.[0];
|
|
65
|
+
if (
|
|
66
|
+
declaration === undefined ||
|
|
67
|
+
!ts.isTypeAliasDeclaration(declaration) ||
|
|
68
|
+
!ts.isTypeReferenceNode(declaration.type)
|
|
69
|
+
)
|
|
70
|
+
return undefined;
|
|
71
|
+
const referenced = this.checker.getSymbolAtLocation(declaration.type.typeName);
|
|
72
|
+
if (referenced === undefined) return undefined;
|
|
73
|
+
// An `import type { X }` binding is itself an alias symbol; follow it to the declaration.
|
|
74
|
+
return (referenced.flags & ts.SymbolFlags.Alias) !== 0 ? this.checker.getAliasedSymbol(referenced) : referenced;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The `typescript/lib/…` path of a symbol declared in TypeScript's own lib files. */
|
|
78
|
+
libPath(symbol) {
|
|
79
|
+
const fileName = symbol?.declarations?.[0]?.getSourceFile().fileName;
|
|
80
|
+
const relative = fileName === undefined ? undefined : packageRelative(fileName);
|
|
81
|
+
return relative?.startsWith(`${COMPILER_SOURCE}/lib/`) === true ? relative : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A fact without a declaration in the pinned package cannot claim authority: it is `derived`. */
|
|
85
|
+
of(symbol) {
|
|
86
|
+
const path = this.pinnedPath(symbol);
|
|
87
|
+
if (path !== undefined)
|
|
88
|
+
return {
|
|
89
|
+
confidence: 'authoritative',
|
|
90
|
+
sources: [{ source: this.sourceId, pointer: pointerOf(path, symbol.declarations[0]) }]
|
|
91
|
+
};
|
|
92
|
+
const lib = this.libPath(symbol);
|
|
93
|
+
if (lib !== undefined)
|
|
94
|
+
return {
|
|
95
|
+
confidence: 'derived',
|
|
96
|
+
sources: [{ source: COMPILER_SOURCE, pointer: pointerOf(lib, symbol.declarations[0]) }]
|
|
97
|
+
};
|
|
98
|
+
return { confidence: 'derived', sources: [{ source: this.sourceId }] };
|
|
99
|
+
}
|
|
100
|
+
}
|
package/lib/runtime.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Facts the declarations leave out are read from the pinned package's runtime code and cite the
|
|
6
|
+
* line they come from (`echarts/lib/component/dataZoom/installCommon.js:52`). A pattern that is no
|
|
7
|
+
* longer found fails generation: the runtime changed, and the fact must be re-established.
|
|
8
|
+
*/
|
|
9
|
+
function locate(root, packageName, { file, pattern }) {
|
|
10
|
+
const lines = readFileSync(join(root, 'node_modules', packageName, file), 'utf8').split('\n');
|
|
11
|
+
const index = lines.findIndex((line) => line.includes(pattern));
|
|
12
|
+
if (index === -1) throw new Error(`${packageName} runtime changed: "${pattern}" not found in ${file}`);
|
|
13
|
+
return { lines, index, pointer: `${packageName}/${file}:${String(index + 1)}` };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function runtimePointer(root, packageName, fact) {
|
|
17
|
+
return locate(root, packageName, fact).pointer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The lines after the one matching `pattern`, up to (not including) the first that starts with
|
|
22
|
+
* `end`, and `pointerAt(i)` citing the i-th of them.
|
|
23
|
+
*/
|
|
24
|
+
export function runtimeSection(root, packageName, { end, ...fact }) {
|
|
25
|
+
const { lines, index, pointer } = locate(root, packageName, fact);
|
|
26
|
+
const length = lines.slice(index + 1).findIndex((line) => line.trimStart().startsWith(end));
|
|
27
|
+
if (length === -1) throw new Error(`${packageName} runtime changed: no "${end}" after ${pointer}`);
|
|
28
|
+
const pointerAt = (offset) => `${packageName}/${fact.file}:${String(index + 2 + offset)}`;
|
|
29
|
+
return { pointer, lines: lines.slice(index + 1, index + 1 + length), pointerAt };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An authoritative provenance record citing one runtime line of the pinned package. */
|
|
33
|
+
export function runtimeProvenance(root, { sourceId, packageName }, fact) {
|
|
34
|
+
return {
|
|
35
|
+
confidence: 'authoritative',
|
|
36
|
+
sources: [{ source: sourceId, pointer: runtimePointer(root, packageName, fact) }]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolveIn } from './nodes.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chart.js resolves every option through its scriptable resolver (`_descriptors` in
|
|
5
|
+
* helpers.dataset.js defaults to `scriptable: true`; the root descriptor is
|
|
6
|
+
* `_scriptable: (name) => !name.startsWith('on')`), so a function is accepted wherever an option
|
|
7
|
+
* value is, whether or not the declarations say `Scriptable<…>`. The few options a plugin marks
|
|
8
|
+
* non-scriptable (`spacing`, callbacks that are functions anyway) are over-accepted: a false
|
|
9
|
+
* negative, never a false rejection.
|
|
10
|
+
*
|
|
11
|
+
* This pass rewrites the IR reachable from each configuration's `options`: every property whose
|
|
12
|
+
* node does not already accept a function becomes `scriptable`. Union discriminators (`type`)
|
|
13
|
+
* stay literal so variants can still be selected.
|
|
14
|
+
*/
|
|
15
|
+
export function makeOptionsScriptable(definitions, configRefs) {
|
|
16
|
+
const discriminators = new Set();
|
|
17
|
+
const collect = (node) => {
|
|
18
|
+
if (node === null || typeof node !== 'object') return;
|
|
19
|
+
if (node.kind === 'union' && node.discriminator !== undefined) discriminators.add(node.discriminator);
|
|
20
|
+
for (const child of Object.values(node)) collect(child);
|
|
21
|
+
};
|
|
22
|
+
collect(definitions);
|
|
23
|
+
|
|
24
|
+
const resolve = resolveIn(definitions);
|
|
25
|
+
const acceptsFunction = (node, seen = new Set()) => {
|
|
26
|
+
if (node.kind === 'ref') {
|
|
27
|
+
if (seen.has(node.ref)) return false;
|
|
28
|
+
seen.add(node.ref);
|
|
29
|
+
}
|
|
30
|
+
const target = resolve(node);
|
|
31
|
+
if (['function', 'scriptable', 'unknown'].includes(target.kind)) return true;
|
|
32
|
+
return target.kind === 'union' && target.anyOf.some((member) => acceptsFunction(member, seen));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Nodes below `target`: included definitions, then members, items, values; a literal's `value` is not a node. */
|
|
36
|
+
const childrenOf = (target) => {
|
|
37
|
+
const included = Array.isArray(target.includes) ? target.includes.map((ref) => ({ kind: 'ref', ref })) : [];
|
|
38
|
+
const nested = [...(target.anyOf ?? []), target.items, target.values, target.value];
|
|
39
|
+
return [...included, ...nested.filter((candidate) => typeof candidate?.kind === 'string')];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const visited = new Set();
|
|
43
|
+
const visit = (node) => {
|
|
44
|
+
if (node.kind === 'ref') {
|
|
45
|
+
if (visited.has(node.ref)) return;
|
|
46
|
+
visited.add(node.ref);
|
|
47
|
+
}
|
|
48
|
+
const target = resolve(node);
|
|
49
|
+
for (const [name, property] of target.kind === 'object' ? Object.entries(target.properties) : []) {
|
|
50
|
+
visit(property.node);
|
|
51
|
+
if (!discriminators.has(name) && !acceptsFunction(property.node))
|
|
52
|
+
property.node = { kind: 'scriptable', value: property.node };
|
|
53
|
+
}
|
|
54
|
+
for (const child of childrenOf(target)) visit(child);
|
|
55
|
+
};
|
|
56
|
+
for (const ref of configRefs) visit(definitions[ref].properties.options.node);
|
|
57
|
+
}
|
package/lib/trace.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { sourceDirectory } from './lock.mjs';
|
|
5
|
+
|
|
6
|
+
const POINTER = /^(?<path>[^:]+):(?<line>[1-9]\d*)$/u;
|
|
7
|
+
/** The name a property declaration line starts with (`readonly 'name'?: …`, `name(…)`). */
|
|
8
|
+
const DECLARED = /^\s*(?:readonly\s+)?['"]?(?<name>[\w$]+)['"]?\??\s*[:(<]/u;
|
|
9
|
+
/** A mapped type (`[P in keyof T]`): the keys it synthesizes cite it, not a line of their own. */
|
|
10
|
+
const MAPPED = /\[\s*[\w$]+\s+in\s/u;
|
|
11
|
+
const IDENTIFIER = /[\w$]/u;
|
|
12
|
+
|
|
13
|
+
/** `chart.js` for `chart.js@4.5.1`, `@scope/name` for `@scope/name@1.0.0`. */
|
|
14
|
+
const packageOf = (locator) => locator.slice(0, locator.lastIndexOf('@'));
|
|
15
|
+
|
|
16
|
+
/** Whether `line` names `name` as a whole word (not inside a longer identifier). */
|
|
17
|
+
function names(line, name) {
|
|
18
|
+
for (let at = line.indexOf(name); at !== -1; at = line.indexOf(name, at + 1)) {
|
|
19
|
+
const before = line[at - 1] ?? ' ';
|
|
20
|
+
const after = line[at + name.length] ?? ' ';
|
|
21
|
+
if (!IDENTIFIER.test(before) && !IDENTIFIER.test(after)) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the declaration a property cites at `index` declares `name`: the line names it, or a key
|
|
28
|
+
* remapping derives it (`pointRadius` from `radius` under `point${Capitalize<K>}`), or a mapped type
|
|
29
|
+
* there synthesizes it.
|
|
30
|
+
*/
|
|
31
|
+
function declares(lines, index, name) {
|
|
32
|
+
const line = lines[index];
|
|
33
|
+
if (names(line, name)) return true;
|
|
34
|
+
const declared = DECLARED.exec(line)?.groups?.name;
|
|
35
|
+
if (declared !== undefined && name.endsWith(declared[0].toUpperCase() + declared.slice(1))) return true;
|
|
36
|
+
return MAPPED.test(`${line}\n${lines[index + 1] ?? ''}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The child nodes of a node, by its kind (D17: structure comes from kinds and known fields, never from key names). */
|
|
40
|
+
function childrenOf(node, path) {
|
|
41
|
+
switch (node.kind) {
|
|
42
|
+
case 'record':
|
|
43
|
+
return [{ node: node.values, path: `${path}.values` }];
|
|
44
|
+
case 'array':
|
|
45
|
+
return [{ node: node.items, path: `${path}.items` }];
|
|
46
|
+
case 'scriptable':
|
|
47
|
+
return [{ node: node.value, path: `${path}.value` }];
|
|
48
|
+
case 'union':
|
|
49
|
+
return node.anyOf.map((member, index) => ({ node: member, path: `${path}.anyOf[${String(index)}]` }));
|
|
50
|
+
default:
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Every provenance record of an artifact (of a node, or of a property with its name). */
|
|
56
|
+
function* records(artifact) {
|
|
57
|
+
const stack = [
|
|
58
|
+
{ node: artifact.root, path: 'root' },
|
|
59
|
+
...Object.entries(artifact.definitions).map(([name, node]) => ({ node, path: `definitions.${name}` }))
|
|
60
|
+
];
|
|
61
|
+
while (stack.length > 0) {
|
|
62
|
+
const { node, path } = stack.pop();
|
|
63
|
+
if (node.provenance !== undefined) yield { provenance: node.provenance, path };
|
|
64
|
+
for (const [name, property] of node.kind === 'object' ? Object.entries(node.properties) : []) {
|
|
65
|
+
yield { provenance: property.provenance, path: `${path}.properties.${name}`, name };
|
|
66
|
+
stack.push({ node: property.node, path: `${path}.properties.${name}.node` });
|
|
67
|
+
}
|
|
68
|
+
stack.push(...childrenOf(node, path));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reads each file once; `undefined` for one that does not exist. */
|
|
73
|
+
function fileLines() {
|
|
74
|
+
const files = new Map();
|
|
75
|
+
return (path) => {
|
|
76
|
+
if (!files.has(path)) files.set(path, existsSync(path) ? readFileSync(path, 'utf8').split('\n') : undefined);
|
|
77
|
+
return files.get(path);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The lines of the file a pointer names and the index of its line, or why it names none. */
|
|
82
|
+
function citedLine({ source, pointer }, recorded, linesOf) {
|
|
83
|
+
const parts = POINTER.exec(pointer)?.groups;
|
|
84
|
+
if (parts === undefined || recorded === undefined || !parts.path.startsWith(`${recorded.name}/`))
|
|
85
|
+
return { problem: `is not a line of a file of source "${source}"` };
|
|
86
|
+
const relative = parts.path.slice(recorded.name.length + 1);
|
|
87
|
+
const lines = relative.split('/').includes('..') ? undefined : linesOf(join(recorded.directory, relative));
|
|
88
|
+
const index = Number(parts.line) - 1;
|
|
89
|
+
if (lines === undefined || index >= lines.length) return { problem: `names no line of ${recorded.name}` };
|
|
90
|
+
return { lines, index, declaration: relative.endsWith('.d.ts') };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The problem of one source reference, if any. `name`: the property it is the provenance of, when held to its line. */
|
|
94
|
+
function problemOf(reference, recorded, linesOf, name) {
|
|
95
|
+
const { problem, lines, index, declaration } = citedLine(reference, recorded, linesOf);
|
|
96
|
+
if (problem !== undefined) return problem;
|
|
97
|
+
return name !== undefined && declaration && !declares(lines, index, name) ? `does not declare "${name}"` : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The provenance problems of `artifact`, built from the install under `root` (CG-07-06). Each pointer
|
|
102
|
+
* must name a file of the package its source records (the library under `root`, the compiler this
|
|
103
|
+
* generator loads) and a line in it; the declaration an authoritative property cites first must
|
|
104
|
+
* declare that property. A generated artifact is never written with one (D19).
|
|
105
|
+
* @returns {string[]}
|
|
106
|
+
*/
|
|
107
|
+
export function untracedFacts(root, artifact) {
|
|
108
|
+
const recorded = new Map(
|
|
109
|
+
Object.entries(artifact.sources).map(([id, { locator }]) => {
|
|
110
|
+
const name = packageOf(locator);
|
|
111
|
+
return [id, { name, directory: sourceDirectory(root, name) }];
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
const linesOf = fileLines();
|
|
115
|
+
const problems = [];
|
|
116
|
+
for (const { provenance, path, name } of records(artifact))
|
|
117
|
+
for (const [index, reference] of provenance.sources.entries()) {
|
|
118
|
+
if (reference.pointer === undefined) continue;
|
|
119
|
+
const held = index === 0 && provenance.confidence === 'authoritative' ? name : undefined;
|
|
120
|
+
const problem = problemOf(reference, recorded.get(reference.source), linesOf, held);
|
|
121
|
+
if (problem !== undefined) problems.push(`${path}: ${reference.pointer} ${problem}`);
|
|
122
|
+
}
|
|
123
|
+
return problems;
|
|
124
|
+
}
|
package/lib/types.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
import { verifiedCompiler } from './lock.mjs';
|
|
6
|
+
import { upstreamChanged } from './nodes.mjs';
|
|
7
|
+
|
|
8
|
+
const GENERATOR = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
9
|
+
|
|
10
|
+
/** The source id of the TypeScript compiler, whose lib files facts about DOM and built-in types cite. */
|
|
11
|
+
export const COMPILER_SOURCE = 'typescript';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checked lookups into upstream declarations. Each throws with the library name when the
|
|
15
|
+
* declarations no longer have the expected shape, so an upstream change fails generation
|
|
16
|
+
* loudly instead of producing a silently different artifact.
|
|
17
|
+
*/
|
|
18
|
+
export function typeLookups(checker, library) {
|
|
19
|
+
const changed = upstreamChanged(`${library} types`);
|
|
20
|
+
const property = (type, name) => {
|
|
21
|
+
const symbol = checker.getPropertyOfType(type, name);
|
|
22
|
+
if (symbol === undefined) throw changed(`no "${name}" property`);
|
|
23
|
+
return symbol;
|
|
24
|
+
};
|
|
25
|
+
const propertyType = (type, name) => checker.getNonNullableType(checker.getTypeOfSymbol(property(type, name)));
|
|
26
|
+
const indexValueType = (type) => {
|
|
27
|
+
const info = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
28
|
+
if (info === undefined) throw changed('expected a string index signature');
|
|
29
|
+
return info.type;
|
|
30
|
+
};
|
|
31
|
+
const elementType = (arrayType) => {
|
|
32
|
+
const [element] = checker.getTypeArguments(arrayType);
|
|
33
|
+
if (element === undefined) throw changed('expected an array type');
|
|
34
|
+
return element;
|
|
35
|
+
};
|
|
36
|
+
return { checker, property, propertyType, indexValueType, elementType };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The members of a union type, or the type itself. */
|
|
40
|
+
export function unionMembers(type) {
|
|
41
|
+
return type.isUnion() ? type.types : [type];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The artifact header shared by every generator; only the library-specific parts differ. It names
|
|
46
|
+
* everything the artifact is built from: the library, the compiler that read it, and the generator.
|
|
47
|
+
*/
|
|
48
|
+
export function artifactOf({ library, sourceId, pin, definitions, root }) {
|
|
49
|
+
const compiler = verifiedCompiler(ts.version);
|
|
50
|
+
return {
|
|
51
|
+
irVersion: 1,
|
|
52
|
+
library,
|
|
53
|
+
libraryVersion: pin.version,
|
|
54
|
+
// Not full until official examples pass against the runtime (CG-01-08).
|
|
55
|
+
coverage: 'partial',
|
|
56
|
+
generator: { name: GENERATOR.name, version: GENERATOR.version },
|
|
57
|
+
sources: {
|
|
58
|
+
[sourceId]: {
|
|
59
|
+
kind: 'npm-package',
|
|
60
|
+
locator: `${pin.package}@${pin.version}`,
|
|
61
|
+
integrity: pin.integrity,
|
|
62
|
+
license: pin.license.spdx
|
|
63
|
+
},
|
|
64
|
+
[COMPILER_SOURCE]: {
|
|
65
|
+
kind: 'npm-package',
|
|
66
|
+
locator: `${compiler.package}@${compiler.version}`,
|
|
67
|
+
integrity: compiler.integrity,
|
|
68
|
+
license: compiler.license.spdx
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
definitions,
|
|
72
|
+
root
|
|
73
|
+
};
|
|
74
|
+
}
|