ggaction 0.0.9 → 0.0.10
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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/knowledge/extension-authoring.md +248 -0
- package/package.json +2 -1
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/extension.js +8 -1
- package/types/extension.d.ts +19 -0
- package/types/program.d.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.0.10] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added collision-safe import-time extension registration through
|
|
12
|
+
`registerExtension({ name, actions })`, including strict TypeScript module
|
|
13
|
+
augmentation for registered actions on the standard `chart()` program.
|
|
14
|
+
- Added installed extension-authoring knowledge that guides LLM agents through
|
|
15
|
+
feature-first design, current ggaction reuse, lifecycle ownership, primitive
|
|
16
|
+
parity, and package-consumer verification.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Updated extension documentation and examples to make installable packages
|
|
21
|
+
compose through registration while retaining `ChartProgram` subclasses for
|
|
22
|
+
deliberately isolated programs.
|
|
23
|
+
|
|
7
24
|
## [0.0.9] - 2026-08-10
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -197,6 +214,7 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
197
214
|
- Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
|
|
198
215
|
not imply complete polar rendering.
|
|
199
216
|
|
|
217
|
+
[0.0.10]: https://github.com/ggaction/ggaction/releases/tag/v0.0.10
|
|
200
218
|
[0.0.9]: https://github.com/ggaction/ggaction/releases/tag/v0.0.9
|
|
201
219
|
[0.0.8]: https://github.com/ggaction/ggaction/releases/tag/v0.0.8
|
|
202
220
|
[0.0.7]: https://github.com/ggaction/ggaction/releases/tag/v0.0.7
|
package/README.md
CHANGED
|
@@ -198,7 +198,7 @@ The package is ESM-only and requires Node.js 20 or later.
|
|
|
198
198
|
| --- | --- |
|
|
199
199
|
| `ggaction` | Create chart programs and render them to Browser Canvas |
|
|
200
200
|
| `ggaction/basic` | Create and render scatter, line, bar, histogram, and heatmap charts with a smaller browser bundle |
|
|
201
|
-
| `ggaction/extension` | Author wrapped actions with public low-level primitives |
|
|
201
|
+
| `ggaction/extension` | Author and register wrapped actions with public low-level primitives |
|
|
202
202
|
| `ggaction/png` | Render a completed program to a PNG file in Node.js |
|
|
203
203
|
| `ggaction/pdf` | Render a completed program to a single-page vector PDF file in Node.js |
|
|
204
204
|
| `ggaction/svg` | Serialize a completed program to browser-safe SVG |
|
|
@@ -229,7 +229,7 @@ the extra discussion required before public API or architecture changes.
|
|
|
229
229
|
|
|
230
230
|
## Status and development
|
|
231
231
|
|
|
232
|
-
> **Status:** `0.0.
|
|
232
|
+
> **Status:** `0.0.10` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
235
|
npm install
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Extension Authoring Knowledge
|
|
2
|
+
|
|
3
|
+
## Purpose and authority
|
|
4
|
+
|
|
5
|
+
This is the compact, installed guide for LLM agents that build extension
|
|
6
|
+
packages against this copy of ggaction. It is ordinary package knowledge, not
|
|
7
|
+
an `AGENTS.md` file, so an extension repository must explicitly tell its agent
|
|
8
|
+
to read it.
|
|
9
|
+
|
|
10
|
+
Use the version in [`package.json`](../package.json) as the compatibility
|
|
11
|
+
baseline. For exact current behavior, the installed runtime and declarations
|
|
12
|
+
take precedence over this guide. Do not substitute behavior from the mutable
|
|
13
|
+
GitHub default branch, a roadmap, or a different installed version.
|
|
14
|
+
|
|
15
|
+
## Minimum intake
|
|
16
|
+
|
|
17
|
+
Before proposing or implementing an extension action:
|
|
18
|
+
|
|
19
|
+
1. Read [`types/extension.d.ts`](../types/extension.d.ts) and
|
|
20
|
+
[`src/extension.js`](../src/extension.js) to confirm the installed public
|
|
21
|
+
extension entry point.
|
|
22
|
+
2. Search [`action-cards.json`](./action-cards.json) for actions whose names,
|
|
23
|
+
intents, owned resources, or prerequisites overlap the requested feature.
|
|
24
|
+
Read only relevant cards instead of loading the complete catalog into the
|
|
25
|
+
working context.
|
|
26
|
+
3. Inspect the installed declaration and source for each reuse candidate when
|
|
27
|
+
the action card does not settle a signature, default, error, ownership, or
|
|
28
|
+
interaction question.
|
|
29
|
+
4. Record in the extension specification which ggaction actions are reused,
|
|
30
|
+
which domain behavior remains new, and the installed ggaction version used
|
|
31
|
+
for that decision.
|
|
32
|
+
|
|
33
|
+
[`task-resolver.js`](./task-resolver.js) may help decompose a concrete chart
|
|
34
|
+
request into current built-in actions. Its result is chart-authoring guidance,
|
|
35
|
+
not authority for extension APIs or unsupported core behavior.
|
|
36
|
+
|
|
37
|
+
## Design from the feature, then the actions
|
|
38
|
+
|
|
39
|
+
Start with the visualization the extension must make possible and its accepted
|
|
40
|
+
data. Derive action boundaries only after that feature contract is clear.
|
|
41
|
+
|
|
42
|
+
- Reuse a current public action when it already owns the required behavior.
|
|
43
|
+
- Compose public domain actions for existing chart resources instead of
|
|
44
|
+
recreating their scale, guide, layout, mark, transform, or renderer logic.
|
|
45
|
+
- Add an extension action only for domain behavior that current actions cannot
|
|
46
|
+
express coherently.
|
|
47
|
+
- Use extension primitives only inside a domain action that owns the resulting
|
|
48
|
+
semantic and graphical lifecycle. Primitives are not a replacement chart
|
|
49
|
+
authoring surface.
|
|
50
|
+
- Never call private helpers or internal wrapped actions from an extension.
|
|
51
|
+
- Require an explicit resource ID when existing state does not identify one
|
|
52
|
+
unique dataset, mark, scale, coordinate, guide, or graphic.
|
|
53
|
+
|
|
54
|
+
## Freeze the executable design before code
|
|
55
|
+
|
|
56
|
+
Before implementation, record one complete vertical slice in the extension
|
|
57
|
+
specification:
|
|
58
|
+
|
|
59
|
+
- the accepted input forms, canonical domain representation, and deterministic
|
|
60
|
+
conversions between them;
|
|
61
|
+
- the shortest complete public program that demonstrates the feature;
|
|
62
|
+
- an ordered action hierarchy that distinguishes reused public actions from new
|
|
63
|
+
extension actions and records conditional children;
|
|
64
|
+
- the canonical owner of every semantic value, derived resource, and graphic;
|
|
65
|
+
and
|
|
66
|
+
- the expected create, repeat, edit, remove, data-change, and relevant
|
|
67
|
+
Canvas/scale lifecycle.
|
|
68
|
+
|
|
69
|
+
Do not begin dependent implementation while the slice still requires an
|
|
70
|
+
unresolved core primitive, renderer capability, semantic path, ownership rule,
|
|
71
|
+
or composition mechanism. Keep that requirement as an explicit upstream
|
|
72
|
+
decision instead of inventing a local substitute.
|
|
73
|
+
|
|
74
|
+
## Composition and ownership
|
|
75
|
+
|
|
76
|
+
A high-level extension action must call a reusable public action when that
|
|
77
|
+
action owns the required validation, inference, state, or materialization. Do
|
|
78
|
+
not flatten an action hierarchy into copied validators, name matching, or
|
|
79
|
+
parallel state updates. The parent should remain thin and its meaningful child
|
|
80
|
+
calls should appear in deterministic trace order.
|
|
81
|
+
|
|
82
|
+
Give each capability one canonical resolver and one canonical state owner.
|
|
83
|
+
Store requested, inferred, and default values where downstream consumers can
|
|
84
|
+
trace them; do not duplicate the same semantic fact in several resources.
|
|
85
|
+
Omitted targets may resolve only from an explicit current owner or one unique
|
|
86
|
+
compatible owner. Missing and ambiguous ownership are different errors, and
|
|
87
|
+
neither permits selecting the first candidate.
|
|
88
|
+
|
|
89
|
+
Type-checking an action hierarchy is not runtime closure. Exercise the complete
|
|
90
|
+
program with representative data so that every prerequisite, target, derived
|
|
91
|
+
resource, guide, and renderer route is real and connected.
|
|
92
|
+
|
|
93
|
+
## Core model
|
|
94
|
+
|
|
95
|
+
- `ChartProgram` is immutable. Return a new program and preserve earlier
|
|
96
|
+
programs and caller-owned input.
|
|
97
|
+
- `semanticSpec` records chart meaning. Fully materialized, backend-neutral
|
|
98
|
+
`graphicSpec` records concrete output.
|
|
99
|
+
- A semantic edit does not compile into graphics automatically. The owning
|
|
100
|
+
domain action must invoke every graphical operation required by its semantic
|
|
101
|
+
change.
|
|
102
|
+
- Renderers consume `graphicSpec`; extension state must not contain
|
|
103
|
+
renderer-specific objects or commands.
|
|
104
|
+
- A user-visible extension action should form one meaningful trace subtree.
|
|
105
|
+
Lower-level wrapped calls belong beneath it.
|
|
106
|
+
|
|
107
|
+
## Lifecycle and atomic failure
|
|
108
|
+
|
|
109
|
+
Validate a normalized complete candidate and every affected downstream
|
|
110
|
+
consumer before returning the first changed program. A rejected action must
|
|
111
|
+
preserve the earlier program, trace, semantic and graphic state, caller-owned
|
|
112
|
+
options, and source rows.
|
|
113
|
+
|
|
114
|
+
When a supported semantic revision changes graphical output, the owning action
|
|
115
|
+
must rebind every affected consumer, rematerialize each required resource once,
|
|
116
|
+
and remove only resources that are truly orphaned. Repeated calls must not
|
|
117
|
+
accumulate geometry, duplicate resources, or revive state removed by an earlier
|
|
118
|
+
action. Removal must clear owned semantic, graphic, guide, selection, highlight,
|
|
119
|
+
and convenience context that would otherwise become stale.
|
|
120
|
+
|
|
121
|
+
If the public extension boundary cannot express a required rebind,
|
|
122
|
+
rematerialization, or safe release, stop at the upstream decision boundary.
|
|
123
|
+
Do not reproduce private lifecycle machinery inside the extension.
|
|
124
|
+
|
|
125
|
+
## Primitive oracle and visual acceptance
|
|
126
|
+
|
|
127
|
+
For new visual behavior, first build one readable primitive baseline. Reuse
|
|
128
|
+
existing public data, mark, encoding, scale, guide, layout, and renderer actions;
|
|
129
|
+
use extension primitives only for the behavior that is genuinely missing.
|
|
130
|
+
Keep pure geometry or statistical expectations in an independent literal
|
|
131
|
+
oracle rather than copying the production implementation.
|
|
132
|
+
|
|
133
|
+
The final public extension program and its primitive baseline must converge on
|
|
134
|
+
the same semantic result, concrete `graphicSpec`, drawing order, and applicable
|
|
135
|
+
renderer calls. For one representative contract, compare decoded pixels when
|
|
136
|
+
the renderer permits deterministic raster evidence. A renderer completing
|
|
137
|
+
without throwing is not sufficient: also assert item cardinality, finite
|
|
138
|
+
geometry, topology, bounds, clipping, and value-to-geometry invariants.
|
|
139
|
+
|
|
140
|
+
When a feature introduces a new appearance or layout, treat representative
|
|
141
|
+
visual review as a separate acceptance gate. Structural tests cannot decide
|
|
142
|
+
whether spacing, alignment, hierarchy, or readability matches the intended
|
|
143
|
+
visual contract.
|
|
144
|
+
|
|
145
|
+
## Current action-authoring boundary
|
|
146
|
+
|
|
147
|
+
Import `action` and `registerExtension` from `ggaction/extension`. An installable
|
|
148
|
+
extension package defines all methods with `action()` and calls
|
|
149
|
+
`registerExtension({ name, actions })` once when its entry module is imported.
|
|
150
|
+
The package must mark that entry as a side effect so bundlers preserve the
|
|
151
|
+
registration import.
|
|
152
|
+
|
|
153
|
+
Registration adds actions only to the complete `chart()` program from
|
|
154
|
+
`ggaction`; it never changes `ggaction/basic`. Each action key must equal the
|
|
155
|
+
wrapped action's `op`. Extension names, built-in and internal program names, and
|
|
156
|
+
previously registered action names cannot collide. The complete action map is
|
|
157
|
+
validated before any method is installed, and non-conflicting extension
|
|
158
|
+
packages must work in either import order.
|
|
159
|
+
|
|
160
|
+
Every wrapped action:
|
|
161
|
+
|
|
162
|
+
- has a stable, non-empty `op` and `description`;
|
|
163
|
+
- accepts one plain options object;
|
|
164
|
+
- runs with the entered immutable program as `this`;
|
|
165
|
+
- returns an instance of the same `ChartProgram` subclass; and
|
|
166
|
+
- leaves a successful result with an empty action stack.
|
|
167
|
+
|
|
168
|
+
For strict TypeScript, augment `RegisteredExtensionActions` in the
|
|
169
|
+
`ggaction/extension` module with each property typed as its exact wrapped action.
|
|
170
|
+
Confirm the exact generic and return types in the installed
|
|
171
|
+
[`types/extension.d.ts`](../types/extension.d.ts) instead of copying a signature
|
|
172
|
+
from another version.
|
|
173
|
+
|
|
174
|
+
`ChartProgram` remains available for deliberately isolated local subclasses.
|
|
175
|
+
Do not mutate its shared prototype directly; installable packages use
|
|
176
|
+
`registerExtension()` so registration is checked and composable.
|
|
177
|
+
|
|
178
|
+
The low-level primitives available through `ChartProgram` are:
|
|
179
|
+
|
|
180
|
+
- `editSemantic` for a supported semantic path;
|
|
181
|
+
- `createGraphics` for backend-neutral graphic identity and hierarchy; and
|
|
182
|
+
- `editGraphics` for concrete graphic values or removal.
|
|
183
|
+
|
|
184
|
+
Use the installed declarations and validation behavior to determine supported
|
|
185
|
+
paths, graphic types, properties, and values. Do not invent opaque semantic
|
|
186
|
+
branches, graphic properties, renderer instructions, or automatic
|
|
187
|
+
materialization.
|
|
188
|
+
|
|
189
|
+
## Upstream decision boundary
|
|
190
|
+
|
|
191
|
+
Stop extension implementation and propose a ggaction core change when the
|
|
192
|
+
feature requires any of the following:
|
|
193
|
+
|
|
194
|
+
- a semantic path or persisted value the installed core rejects;
|
|
195
|
+
- a new shared graphic primitive or renderer capability;
|
|
196
|
+
- access to a private or internal action;
|
|
197
|
+
- a change to immutable state, trace, materialization, or renderer boundaries;
|
|
198
|
+
- a change to the public extension registration or composition mechanism; or
|
|
199
|
+
- behavior that would be duplicated inconsistently across extensions.
|
|
200
|
+
|
|
201
|
+
Keep that core proposal separate from the extension implementation. Do not hide
|
|
202
|
+
the missing core contract behind monkey-patching, source-relative imports,
|
|
203
|
+
renderer-specific state, or copied internal code.
|
|
204
|
+
|
|
205
|
+
## LLM working discipline
|
|
206
|
+
|
|
207
|
+
- Read the installed compact knowledge needed for the current feature; do not
|
|
208
|
+
preload complete documentation or copy rules from historical roadmaps.
|
|
209
|
+
- When using the task resolver, submit only the exact user request. Do not add
|
|
210
|
+
dataset contents or code scaffolding to the query.
|
|
211
|
+
- Make specifications and examples executable without guesswork: include exact
|
|
212
|
+
public imports, program construction, data and Canvas prerequisites,
|
|
213
|
+
immutable chaining or reassignment, and the requested renderer call.
|
|
214
|
+
- Treat a known unsupported capability as terminal. Keep a decision that needs
|
|
215
|
+
user input or a bounded reference read explicitly unresolved; never hide
|
|
216
|
+
either case behind a nearby action or silent partial output.
|
|
217
|
+
- Keep failure feedback bounded and actionable. Preserve the original failure
|
|
218
|
+
category while reporting the smallest public correction path.
|
|
219
|
+
|
|
220
|
+
## Required evidence
|
|
221
|
+
|
|
222
|
+
For each new public extension action, verify at least:
|
|
223
|
+
|
|
224
|
+
- the public package entry works from an installed-package consumer;
|
|
225
|
+
- importing the package registers every declared action on `chart()` but not
|
|
226
|
+
`ggaction/basic`;
|
|
227
|
+
- registration rejects collisions and invalid batches without partial changes,
|
|
228
|
+
and non-conflicting packages work in either import order;
|
|
229
|
+
- strict TypeScript sees the exact registered action methods through
|
|
230
|
+
`RegisteredExtensionActions`;
|
|
231
|
+
- earlier programs and caller-owned input remain unchanged;
|
|
232
|
+
- semantic and graphic state match the feature specification;
|
|
233
|
+
- the trace has the intended root action and wrapped children;
|
|
234
|
+
- the shortest valid call, boundary values, empty or missing input, invalid
|
|
235
|
+
input, ambiguity, repeated calls, supported lifecycle changes, and recovery
|
|
236
|
+
after failure behave explicitly;
|
|
237
|
+
- semantic revisions leave no stale binding, graphic, guide, context, or
|
|
238
|
+
duplicate resource;
|
|
239
|
+
- a primitive baseline and public extension program satisfy their declared
|
|
240
|
+
semantic, graphic, order, renderer-call, and representative visual parity;
|
|
241
|
+
- failures are deterministic and explain how to correct invalid or ambiguous
|
|
242
|
+
input; and
|
|
243
|
+
- representative rendering succeeds in every renderer the extension claims to
|
|
244
|
+
support.
|
|
245
|
+
|
|
246
|
+
Keep the extension specification, implementation, declarations, package
|
|
247
|
+
exports, tests, examples, and user documentation synchronized as one
|
|
248
|
+
user-facing change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ggaction",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "Build charts through immutable, traceable graphical actions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hyeon Jeon",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"src/",
|
|
26
26
|
"!src/**/AGENTS.md",
|
|
27
27
|
"knowledge/action-cards.json",
|
|
28
|
+
"knowledge/extension-authoring.md",
|
|
28
29
|
"knowledge/intent-taxonomy.json",
|
|
29
30
|
"knowledge/mcp-resources.json",
|
|
30
31
|
"knowledge/task-packet.schema.json",
|
package/src/core/action.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
|
|
2
2
|
|
|
3
|
+
const metadataByWrappedAction = new WeakMap();
|
|
4
|
+
|
|
5
|
+
export function getWrappedActionMetadata(value) {
|
|
6
|
+
return metadataByWrappedAction.get(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
function summarizeObject(value, ancestors = new WeakSet()) {
|
|
4
10
|
if (ancestors.has(value)) {
|
|
5
11
|
throw new TypeError("Action arguments must not contain circular references.");
|
|
@@ -107,24 +113,33 @@ export function action(metadata, implementation) {
|
|
|
107
113
|
throw new Error(`Unknown action scope "${scope}".`);
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
const ownedMetadata = freezeOwned({
|
|
117
|
+
op: metadata.op,
|
|
118
|
+
description: metadata.description,
|
|
119
|
+
scope
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const wrappedAction = function wrappedAction(args = {}) {
|
|
111
123
|
if (!isPlainObject(args)) {
|
|
112
124
|
throw new TypeError("Action arguments must be a plain object.");
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
if (scope === "unit") this._assertUnitProgram(
|
|
116
|
-
if (scope === "composition") this._assertCompositionProgram(
|
|
127
|
+
if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
|
|
128
|
+
if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
|
|
117
129
|
|
|
118
130
|
const entered = this._enterAction({
|
|
119
|
-
...
|
|
131
|
+
...ownedMetadata,
|
|
120
132
|
args: summarizeArgs(args)
|
|
121
133
|
});
|
|
122
134
|
const result = implementation.call(entered, args);
|
|
123
135
|
|
|
124
136
|
if (!(result instanceof this.constructor)) {
|
|
125
|
-
throw new TypeError(`${
|
|
137
|
+
throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
return result._exitAction();
|
|
129
141
|
};
|
|
142
|
+
|
|
143
|
+
metadataByWrappedAction.set(wrappedAction, ownedMetadata);
|
|
144
|
+
return wrappedAction;
|
|
130
145
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getWrappedActionMetadata } from "./action.js";
|
|
2
|
+
import { isPlainObject } from "./immutable.js";
|
|
3
|
+
|
|
4
|
+
const ACTION_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
const DEFINITION_KEYS = new Set(["name", "actions"]);
|
|
6
|
+
const registeredExtensionNames = new Set();
|
|
7
|
+
|
|
8
|
+
function readDataProperty(object, key, owner) {
|
|
9
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
10
|
+
if (descriptor === undefined) {
|
|
11
|
+
throw new TypeError(`${owner} requires a ${key} property.`);
|
|
12
|
+
}
|
|
13
|
+
if (!("value" in descriptor)) {
|
|
14
|
+
throw new TypeError(`${owner} ${key} must be a data property.`);
|
|
15
|
+
}
|
|
16
|
+
return descriptor.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateDefinitionShape(definition) {
|
|
20
|
+
if (!isPlainObject(definition)) {
|
|
21
|
+
throw new TypeError("Extension definition must be a plain object.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const key of Reflect.ownKeys(definition)) {
|
|
25
|
+
if (typeof key !== "string" || !DEFINITION_KEYS.has(key)) {
|
|
26
|
+
throw new TypeError(`Unknown extension definition property "${String(key)}".`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const name = readDataProperty(definition, "name", "Extension definition");
|
|
31
|
+
const actions = readDataProperty(
|
|
32
|
+
definition,
|
|
33
|
+
"actions",
|
|
34
|
+
"Extension definition"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
typeof name !== "string" ||
|
|
39
|
+
name.length === 0 ||
|
|
40
|
+
name !== name.trim()
|
|
41
|
+
) {
|
|
42
|
+
throw new TypeError("Extension name must be a non-empty trimmed string.");
|
|
43
|
+
}
|
|
44
|
+
if (!isPlainObject(actions)) {
|
|
45
|
+
throw new TypeError("Extension actions must be a plain object.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { name, actions };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerProgramExtension(ProgramClass, definition) {
|
|
52
|
+
const { name, actions } = validateDefinitionShape(definition);
|
|
53
|
+
|
|
54
|
+
if (registeredExtensionNames.has(name)) {
|
|
55
|
+
throw new Error(`Extension "${name}" is already registered.`);
|
|
56
|
+
}
|
|
57
|
+
if (!Object.isExtensible(ProgramClass.prototype)) {
|
|
58
|
+
throw new Error("ChartProgram prototype does not accept extension actions.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const program = new ProgramClass();
|
|
62
|
+
const descriptors = Object.create(null);
|
|
63
|
+
const actionNames = Reflect.ownKeys(actions);
|
|
64
|
+
if (actionNames.length === 0) {
|
|
65
|
+
throw new TypeError("Extension actions must contain at least one action.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const actionName of actionNames) {
|
|
69
|
+
if (
|
|
70
|
+
typeof actionName !== "string" ||
|
|
71
|
+
!ACTION_NAME_PATTERN.test(actionName)
|
|
72
|
+
) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`Extension action name "${String(actionName)}" must be a JavaScript identifier.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const action = readDataProperty(actions, actionName, "Extension actions");
|
|
79
|
+
const metadata = getWrappedActionMetadata(action);
|
|
80
|
+
if (metadata === undefined) {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`Extension action "${actionName}" must be created with action().`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (metadata.op !== actionName) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Extension action "${actionName}" must use the same action op.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (actionName in program) {
|
|
91
|
+
throw new Error(`ChartProgram action "${actionName}" is already defined.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
descriptors[actionName] = {
|
|
95
|
+
value: action,
|
|
96
|
+
configurable: false,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
writable: false
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Object.defineProperties(ProgramClass.prototype, descriptors);
|
|
103
|
+
registeredExtensionNames.add(name);
|
|
104
|
+
}
|
package/src/extension.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export { action } from "./core/action.js";
|
|
2
|
-
|
|
2
|
+
import { ChartProgram } from "./ChartProgram.js";
|
|
3
|
+
import { registerProgramExtension } from "./core/extensionRegistry.js";
|
|
4
|
+
|
|
5
|
+
export { ChartProgram };
|
|
6
|
+
|
|
7
|
+
export function registerExtension(definition) {
|
|
8
|
+
registerProgramExtension(ChartProgram, definition);
|
|
9
|
+
}
|
package/types/extension.d.ts
CHANGED
|
@@ -15,6 +15,21 @@ export interface ActionMetadata {
|
|
|
15
15
|
scope?: "unit" | "composition" | "any";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface RegisteredExtensionActions {}
|
|
19
|
+
|
|
20
|
+
export type RegisteredExtensionAction = (
|
|
21
|
+
this: ChartProgram,
|
|
22
|
+
...args: any[]
|
|
23
|
+
) => ChartProgram;
|
|
24
|
+
|
|
25
|
+
export interface ExtensionDefinition<
|
|
26
|
+
TActions extends Readonly<Record<string, RegisteredExtensionAction>> =
|
|
27
|
+
Readonly<Record<string, RegisteredExtensionAction>>
|
|
28
|
+
> {
|
|
29
|
+
name: string;
|
|
30
|
+
actions: TActions;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export function action<TOptions extends ActionOptions = ActionOptions>(
|
|
19
34
|
metadata: ActionMetadata,
|
|
20
35
|
implementation: (this: ChartProgram, options: TOptions) => ChartProgram
|
|
@@ -22,3 +37,7 @@ export function action<TOptions extends ActionOptions = ActionOptions>(
|
|
|
22
37
|
this: TProgram,
|
|
23
38
|
options?: TOptions
|
|
24
39
|
) => TProgram;
|
|
40
|
+
|
|
41
|
+
export function registerExtension<
|
|
42
|
+
TActions extends Readonly<Record<string, RegisteredExtensionAction>>
|
|
43
|
+
>(definition: ExtensionDefinition<TActions>): void;
|
package/types/program.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RegisteredExtensionActions } from "./extension.js";
|
|
2
|
+
|
|
1
3
|
export type FieldType = "quantitative" | "temporal" | "ordinal" | "nominal";
|
|
2
4
|
export type GraphicType =
|
|
3
5
|
| "canvas"
|
|
@@ -2189,6 +2191,8 @@ export interface EditTitleOptions
|
|
|
2189
2191
|
subtitle?: string | false;
|
|
2190
2192
|
}
|
|
2191
2193
|
|
|
2194
|
+
export interface ChartProgram extends RegisteredExtensionActions {}
|
|
2195
|
+
|
|
2192
2196
|
export class ChartProgram {
|
|
2193
2197
|
constructor(state?: ActionOptions);
|
|
2194
2198
|
readonly semanticSpec: SemanticSpec;
|