From b8eb8cd0dfbb94e8531e668f3b21eddc4033474e Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 29 Nov 2024 10:18:42 -0800 Subject: [PATCH 1/5] Migrate PerseusWidgetsMap to use a merged interface to represent all available widgets --- packages/perseus/src/index.ts | 2 + packages/perseus/src/perseus-types.ts | 141 ++++++++++++++------------ 2 files changed, 78 insertions(+), 65 deletions(-) diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index ecf8aab1b7..1bb32a6b61 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -222,7 +222,9 @@ export type { PerseusRenderer, PerseusWidget, PerseusWidgetsMap, + PerseusWidgetTypes, MultiItem, + WidgetOptions, } from "./perseus-types"; export type {Coord} from "./interactive2/types"; export type {MarkerType} from "./widgets/label-image/types"; diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index f31451e81b..e483b32449 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -11,72 +11,83 @@ export type Size = [number, number]; export type CollinearTuple = [vec.Vector2, vec.Vector2]; export type ShowSolutions = "all" | "selected" | "none"; +/** + * Our core set of Perseus widgets. + * + * This interface is the basis for "registering" all Perseus widget types. + * There should be one key/value pair for each supported widget. If you create + * a new widget, an entry should be added to this interface. + * + * Importantly, the key is not important and is not used anywhere outside of + * this interface so it is arbitrary. + * + * If you define the widget outside of this package, you can still add the new + * widget to this interface by writing the following in that pacakge that + * contains the widget. TypeScript will merge that definition of the + * `PerseusWidgets` with the one defined below. + * + * ```typescript + * declare module "@khanacademy/perseus" { + * interface PerseusWidgetTypes { + * Awesomeness: MyAwesomeNewWidget; + * } + * } + * + * type MyAwesomeNewWidget = WidgetOptions<'awesomeness', MyAwesomeNewWidgetOptions>; + * ``` + * + * This interface can be extended through the magic of TypeScript "Declaration + * merging". Specifically, we augment this module and extend this interface. + * + * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation} + */ +export interface PerseusWidgetTypes { + Categorizer: CategorizerWidget; + CSProgram: CSProgramWidget; + Definition: DefinitionWidget; + Dropdown: DropdownWidget; + Explanation: ExplanationWidget; + Expression: ExpressionWidget; + Grapher: GrapherWidget; + GradedGroupSet: GradedGroupSetWidget; + GradedGroup: GradedGroupWidget; + Group: GroupWidget; + IFrame: IFrameWidget; + Image: ImageWidget; + InputNumber: InputNumberWidget; + Interaction: InteractionWidget; + InteractiveGraph: InteractiveGraphWidget; + LabelImage: LabelImageWidget; + Matcher: MatcherWidget; + Matrix: MatrixWidget; + Measurer: MeasurerWidget; + MoleculeRenderer: MoleculeRendererWidget; + NumberLine: NumberLineWidget; + NumericInput: NumericInputWidget; + Orderer: OrdererWidget; + PassageRefTarget: RefTargetWidget; + PassageRef: PassageRefWidget; + Passage: PassageWidget; + PhetSimulation: PhetSimulationWidget; + Python: PythonProgramWidget; + Plotter: PlotterWidget; + Radio: RadioWidget; + Sorter: SorterWidget; + Table: TableWidget; + Video: VideoWidget; +} + +/** + * A map of widget IDs to widget options. This is most often used as the type + * for a set of widgets defined in a `PerseusItem` but can also be useful to + * represent a function parameter where only `widgets` from a `PerseusItem` are + * needed. + * + * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type + * by augmenting the PerseusWidgetTypes with new widget types! + */ export type PerseusWidgetsMap = { - [key in `categorizer ${number}`]: CategorizerWidget; -} & { - [key in `cs-program ${number}`]: CSProgramWidget; -} & { - [key in `definition ${number}`]: DefinitionWidget; -} & { - [key in `dropdown ${number}`]: DropdownWidget; -} & { - [key in `explanation ${number}`]: ExplanationWidget; -} & { - [key in `expression ${number}`]: ExpressionWidget; -} & { - [key in `grapher ${number}`]: GrapherWidget; -} & { - [key in `group ${number}`]: GroupWidget; -} & { - [key in `graded-group ${number}`]: GradedGroupWidget; -} & { - [key in `graded-group-set ${number}`]: GradedGroupSetWidget; -} & { - [key in `iframe ${number}`]: IFrameWidget; -} & { - [key in `image ${number}`]: ImageWidget; -} & { - [key in `input-number ${number}`]: InputNumberWidget; -} & { - [key in `interaction ${number}`]: InteractionWidget; -} & { - [key in `interactive-graph ${number}`]: InteractiveGraphWidget; -} & { - [key in `label-image ${number}`]: LabelImageWidget; -} & { - [key in `matcher ${number}`]: MatcherWidget; -} & { - [key in `matrix ${number}`]: MatrixWidget; -} & { - [key in `measurer ${number}`]: MeasurerWidget; -} & { - [key in `molecule-renderer ${number}`]: MoleculeRendererWidget; -} & { - [key in `number-line ${number}`]: NumberLineWidget; -} & { - [key in `numeric-input ${number}`]: NumericInputWidget; -} & { - [key in `orderer ${number}`]: OrdererWidget; -} & { - [key in `passage ${number}`]: PassageWidget; -} & { - [key in `passage-ref ${number}`]: PassageRefWidget; -} & { - [key in `passage-ref-target ${number}`]: PassageRefWidget; -} & { - [key in `phet-simulation ${number}`]: PhetSimulationWidget; -} & { - [key in `plotter ${number}`]: PlotterWidget; -} & { - [key in `python-program ${number}`]: PythonProgramWidget; -} & { - [key in `radio ${number}`]: RadioWidget; -} & { - [key in `sorter ${number}`]: SorterWidget; -} & { - [key in `table ${number}`]: TableWidget; -} & { - [key in `video ${number}`]: VideoWidget; + [Property in keyof PerseusWidgetTypes as `${PerseusWidgetTypes[Property]["type"]} ${number}`]: PerseusWidgetTypes[Property]; }; /** From 80721e765cd7c951cb1e7f2c422939387a7bf7b3 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 29 Nov 2024 13:19:27 -0800 Subject: [PATCH 2/5] Changeset --- .changeset/tender-planets-greet.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tender-planets-greet.md diff --git a/.changeset/tender-planets-greet.md b/.changeset/tender-planets-greet.md new file mode 100644 index 0000000000..6430e9226c --- /dev/null +++ b/.changeset/tender-planets-greet.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Changes the PerseusWidgetsMap to be extensible so that widgets can be registered outside of Perseus and still have full type safety. From ee5b07f0b7a76add24c00bc27e3dbe5048a9454e Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 2 Dec 2024 13:40:27 -0800 Subject: [PATCH 3/5] Add note about widget id format --- packages/perseus/src/perseus-types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index e483b32449..abf95eb61b 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -81,7 +81,10 @@ export interface PerseusWidgetTypes { * A map of widget IDs to widget options. This is most often used as the type * for a set of widgets defined in a `PerseusItem` but can also be useful to * represent a function parameter where only `widgets` from a `PerseusItem` are - * needed. + * needed. Today Widget IDs are made up of the widget type and an incrementing + * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid + * reading/parsing the widget id to derive any information from it, except in + * the case of this map. * * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type * by augmenting the PerseusWidgetTypes with new widget types! From 5d5e4158d5ae1c922302dad4c803e2b4350e8bed Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Mon, 2 Dec 2024 14:43:25 -0800 Subject: [PATCH 4/5] Add note that you still need to register widgets --- packages/perseus/src/perseus-types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index abf95eb61b..055699d6e3 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -16,7 +16,10 @@ export type ShowSolutions = "all" | "selected" | "none"; * * This interface is the basis for "registering" all Perseus widget types. * There should be one key/value pair for each supported widget. If you create - * a new widget, an entry should be added to this interface. + * a new widget, an entry should be added to this interface. Note that this + * only registers the widget options type, you'll also need to register the + * widget so that it's available at runtime (@see + * {@link file://./widgets.ts#registerWidget}). * * Importantly, the key is not important and is not used anywhere outside of * this interface so it is arbitrary. From 03ee194a9b581e4c0d897c890289f58d019593c9 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 3 Dec 2024 11:51:31 -0800 Subject: [PATCH 5/5] Update packages/perseus/src/perseus-types.ts Co-authored-by: Matthew --- packages/perseus/src/perseus-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 055699d6e3..94e5677c1e 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -25,7 +25,7 @@ export type ShowSolutions = "all" | "selected" | "none"; * this interface so it is arbitrary. * * If you define the widget outside of this package, you can still add the new - * widget to this interface by writing the following in that pacakge that + * widget to this interface by writing the following in that package that * contains the widget. TypeScript will merge that definition of the * `PerseusWidgets` with the one defined below. *