From ba0f953fcb92aa998a49d20074719911a5c08f96 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Tue, 21 Jul 2020 02:27:18 -0400 Subject: [PATCH 01/46] Add TypeScript definitions. --- index.d.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..14791e9ae --- /dev/null +++ b/index.d.ts @@ -0,0 +1,125 @@ +declare module "hyperapp" { + // A Hyperapp application instance has an initial state and a base view. + // It must also be mounted over an available DOM element. + type App = Readonly<{ + init: Initiator; + view: View; + node: Node; + subscriptions?: Subscription; + middleware?: Middleware; + }> + + // Initially, a state is set with any effects to run, or an action is taken. + type Initiator + = State + | [State, ...EffectDescriptor[]] + | Action + + // A view builds a virtual DOM node representation of the application state. + type View = (state: State) => VDOM + + // Application state is accessible in every view, action, and subscription. + type State = S + + // A subscription is a set of recurring effects. + type Subscription = (state: State) => Subscriber[] + + // A subscriber reacts to subscription updates. + type Subscriber = boolean | void | Effect | Unsubscribe + + // A subscriber ideally provides a function that cancels itself properly. + type Unsubscribe = () => void + + // Middleware allows for custom processing during dispatching. + type Middleware = (dispatch: Dispatch) => Dispatch + + // --------------------------------------------------------------------------- + + // A dispatched action handles an event in the context of the current state. + type Dispatch = (action: Action, props?: Payload

) => void + + // An action transforms existing state while possibly invoking effects and it + // can be wrapped by another action. + type Action + = [Action, Payload

] + | ((state: State, props?: Payload

) + => State + | [State, ...EffectDescriptor[]] + | Action) + + // A payload is data external to state that is given to a dispatched action. + type Payload

= P + + // An effect descriptor describes how an effect should be invoked. + // A function that creates this is called an effect constructor. + type EffectDescriptor = [Effect, EffectData] + + // An effect is where side effects and any additional dispatching occur. + type Effect = (dispatch: Dispatch, props?: EffectData) => void + + // An effect is generally given additional data. + type EffectData = D + + // --------------------------------------------------------------------------- + + // A virtual DOM node represents an actual DOM element. + type VDOM = { + readonly type: string; + readonly props: PropList; + readonly children: VNode[]; + node: MaybeNode; + readonly tag?: Tag; + readonly key: Key; + memo?: PropList; + } + + // Virtual DOM properties will often correspond to HTML attributes. + type Prop = bigint | boolean | null | number | string | symbol | undefined | Function | ClassProp | StyleProp + type PropList = Readonly + + // A key can uniquely associate a virtual DOM node with a certain DOM element. + type Key = null | string | undefined + + // The `class` property represents an HTML class attribute string. + type ClassProp = string | Record | ClassProp[] + + // The `style` property represents inline CSS. + type StyleProp = Record + + // A virtual node is a convenience layer over a virtual DOM node. + type VNode = null | undefined | VDOM + + // Actual DOM nodes will be manipulated depending on how property patching goes. + type MaybeNode = null | undefined | Node + + // A virtual DOM node's tag has metadata relevant to it. Virtual DOM nodes are + // tagged by their type to assist rendering. + type Tag = VDOMNodeType | View + + // These are based on actual DOM node types: + // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + const enum VDOMNodeType { + SSR = 1, + Text = 3, + } + + // --------------------------------------------------------------------------- + + // The `app` function initiates a Hyperapp application. `app` along with effects + // should be the only places you need to worry about side effects. + function app(props: App): Dispatch + + // The `h` function builds a virtual DOM node. + function h(type: string, props: PropList, children?: VNode | readonly VNode[]): VDOM + + // The `memo` function stores a view along with properties for it. + function memo(view: View, props: PropList): Partial + + // The `text` function creates a virtual DOM node representing plain text. + function text(value: string | number, node?: Node): VDOM +} From 286debf05b2846ea22f6a785e6e5087e0330a2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=A4ufler?= Date: Sat, 25 Jul 2020 21:43:39 +0200 Subject: [PATCH 02/46] Nitpick: Rename constant for clarity While the variable name is true in what it describes, I feel it's improved by naming it for what it _means_. --- pkg/html/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/html/index.js b/pkg/html/index.js index a120e31d5..7a09ea88f 100644 --- a/pkg/html/index.js +++ b/pkg/html/index.js @@ -1,6 +1,6 @@ -const EMPTY_ARR = [] +const NO_CHILDREN = [] -const h = (type) => (props, children = EMPTY_ARR) => ({ +const h = (type) => (props, children = NO_CHILDREN) => ({ type, props, children: Array.isArray(children) ? children : [children], From 6f0ba808c189479d5c912759bb498f9bea8b8dda Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 27 Jul 2020 00:42:46 -0400 Subject: [PATCH 03/46] Update `VNode`. --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 14791e9ae..da1802c82 100644 --- a/index.d.ts +++ b/index.d.ts @@ -92,7 +92,7 @@ declare module "hyperapp" { type StyleProp = Record // A virtual node is a convenience layer over a virtual DOM node. - type VNode = null | undefined | VDOM + type VNode = boolean | null | undefined | VDOM // Actual DOM nodes will be manipulated depending on how property patching goes. type MaybeNode = null | undefined | Node From d405ddfddb464acfce29a7c721fee8c874da1336 Mon Sep 17 00:00:00 2001 From: Xie Yuheng Date: Tue, 28 Jul 2020 12:35:41 +0800 Subject: [PATCH 04/46] Use `text(...)` instead of plain `string` in tutorial. --- docs/tutorial.md | 108 +++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 57dd13ce4..50a2ff9c5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1,6 +1,6 @@ # Tutorial -Welcome! If you're new to Hyperapp, you've found the perfect place to start learning. This tutorial will guide you through your first steps with Hyperapp as we build a simple app. +Welcome! If you're new to Hyperapp, you've found the perfect place to start learning. This tutorial will guide you through your first steps with Hyperapp as we build a simple app. - [The Set-up](#setup) - [Hello World](#helloworld) @@ -101,7 +101,7 @@ Create this html file: href="https://hyperapp.dev/tutorial-assets/style.css" /> @@ -167,51 +167,51 @@ To render the HTML we want, change the `view` to: view: () => h("div", { id: "app", class: "container" }, [ h("div", { class: "filter" }, [ - " Filter: ", - h("span", { class: "filter-word" }, "ocean"), - h("button", {}, "\u270E"), + text(" Filter: "), + h("span", { class: "filter-word" }, text("ocean")), + h("button", {}, text("\u270E")), ]), h("div", { class: "stories" }, [ h("ul", {}, [ h("li", { class: "unread" }, [ h("p", { class: "title" }, [ - "The ", - h("em", {}, "Ocean"), - " is Sinking!", + text("The "), + h("em", {}, text("Ocean")), + text(" is Sinking!"), ]), - h("p", { class: "author" }, "Kat Stropher"), + h("p", { class: "author" }, text("Kat Stropher")), ]), h("li", { class: "reading" }, [ - h("p", { class: "title" }, [h("em", {}, "Ocean"), " life is brutal"]), - h("p", { class: "author" }, "Surphy McBrah"), + h("p", { class: "title" }, [h("em", {}, text("Ocean")), text(" life is brutal")]), + h("p", { class: "author" }, text("Surphy McBrah")), ]), h("li", {}, [ h("p", { class: "title" }, [ - "Family friendly fun at the ", - h("em", {}, "ocean"), - " exhibit", + text("Family friendly fun at the "), + h("em", {}, text("ocean")), + text(" exhibit"), ]), - h("p", { class: "author" }, "Guy Prosales"), + h("p", { class: "author" }, text("Guy Prosales")), ]), ]), ]), h("div", { class: "story" }, [ - h("h1", {}, "Ocean life is brutal"), + h("h1", {}, text("Ocean life is brutal")), h( "p", {}, - ` + text(` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - ` + `) ), - h("p", { class: "signature" }, "Surphy McBrah"), + h("p", { class: "signature" }, text("Surphy McBrah")), ]), h("div", { class: "autoupdate" }, [ - "Auto update: ", + text("Auto update: "), h("input", { type: "checkbox" }), ]), ]) @@ -239,7 +239,7 @@ const emphasize = (word, string) => string .split(" ") .map((x) => - x.toLowerCase() === word.toLowerCase() ? h("em", {}, x + " ") : x + " " + x.toLowerCase() === word.toLowerCase() ? h("em", {}, text(x + " ")) : text(x + " ") ) ``` @@ -248,9 +248,9 @@ It lets you change this: ```js ... h("p", {class: "title"}, [ - "The ", - h("em", {}, "Ocean"), - " is Sinking!" + text("The "), + h("em", {}, text("Ocean")), + text(" is Sinking!") ]), ... ``` @@ -280,7 +280,7 @@ const storyThumbnail = (props) => }, [ h("p", { class: "title" }, emphasize(props.filter, props.title)), - h("p", { class: "author" }, props.author), + h("p", { class: "author" }, text(props.author)), ] ) ``` @@ -312,32 +312,32 @@ const storyList = (props) => const filterView = (props) => h("div", { class: "filter" }, [ - "Filter:", - h("span", { class: "filter-word" }, props.filter), - h("button", {}, "\u270E"), + text("Filter:"), + h("span", { class: "filter-word" }, text(props.filter)), + h("button", {}, text("\u270E")), ]) const storyDetail = (props) => h("div", { class: "story" }, [ - props && h("h1", {}, props.title), + props && h("h1", {}, text(props.title)), props && - h( - "p", - {}, - ` + h( + "p", + {}, + text(` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, qui nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - ` - ), - props && h("p", { class: "signature" }, props.author), + `) + ), + props && h("p", { class: "signature" }, text(props.author)), ]) const autoUpdateView = (props) => h("div", { class: "autoupdate" }, [ - "Auto update: ", + text("Auto update: "), h("input", { type: "checkbox" }), ]) @@ -444,9 +444,9 @@ Add an `onclick` property to the button in `filterView`: ```js const filterView = (props) => h("div", { class: "filter" }, [ - "Filter:", - h("span", { class: "filter-word" }, props.filter), - h("button", { onclick: StartEditingFilter }, "\u270E"), // <--- + text("Filter:"), + h("span", { class: "filter-word" }, text(props.filter)), + h("button", { onclick: StartEditingFilter }, text("\u270E")), // <--- ]) ``` @@ -477,9 +477,9 @@ const filterView = (props) => props.editingFilter // <--- ? h("input", { type: "text", value: props.filter }) // <--- - : h("span", { class: "filter-word" }, props.filter), + : h("span", { class: "filter-word" }, text(props.filter)), - h("button", { onclick: StartEditingFilter }, "\u270E"), + h("button", { onclick: StartEditingFilter }, text("\u270E")), ]) ``` @@ -501,11 +501,11 @@ const filterView = (props) => props.editingFilter ? h("input", { type: "text", value: props.filter }) - : h("span", { class: "filter-word" }, props.filter), + : h("span", { class: "filter-word" }, text(props.filter)), props.editingFilter // <--- - ? h("button", { onclick: StopEditingFilter }, "\u2713") - : h("button", { onclick: StartEditingFilter }, "\u270E"), // <--- + ? h("button", { onclick: StopEditingFilter }, text("\u2713")) + : h("button", { onclick: StartEditingFilter }, text("\u270E")), // <--- ]) ``` @@ -531,11 +531,11 @@ const filterView = (props) => value: props.filter, oninput: SetFilter, // <---- }) - : h("span", { class: "filter-word" }, props.filter), + : h("span", { class: "filter-word" }, text(props.filter)), props.editingFilter - ? h("button", { onclick: StopEditingFilter }, "\u2713") - : h("button", { onclick: StartEditingFilter }, "\u270E"), + ? h("button", { onclick: StopEditingFilter }, text("\u2713")) + : h("button", { onclick: StartEditingFilter }, text("\u270E")), ]) ``` @@ -579,7 +579,7 @@ const storyThumbnail = (props) => }, [ h("p", { class: "title" }, emphasize(props.filter, props.title)), - h("p", { class: "author" }, props.author), + h("p", { class: "author" }, text(props.author)), ] ) ``` @@ -631,11 +631,11 @@ const filterView = (props) => value: props.filter, oninput: [SetFilter, (event) => event.target.value], // <---- }) - : h("span", { class: "filter-word" }, props.filter), + : h("span", { class: "filter-word" }, text(props.filter)), props.editingFilter - ? h("button", { onclick: StopEditingFilter }, "\u2713") - : h("button", { onclick: StartEditingFilter }, "\u270E"), + ? h("button", { onclick: StopEditingFilter }, text("\u2713")) + : h("button", { onclick: StartEditingFilter }, text("\u270E")), ]) ``` @@ -917,7 +917,7 @@ Dispatch it in response to checking the checkbox in `autoUpdateView`: ```js const autoUpdateView = (props) => h("div", { class: "autoupdate" }, [ - "Auto update: ", + text("Auto update: "), h("input", { type: "checkbox", checked: props.autoUpdate, // <--- From dcd415510bb7d6448ec62c830d90e726e1a29e2f Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 17 Aug 2020 18:18:26 -0400 Subject: [PATCH 05/46] Let `ClassProp` know when a class is turned off. --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index da1802c82..b3c0d8e91 100644 --- a/index.d.ts +++ b/index.d.ts @@ -86,7 +86,7 @@ declare module "hyperapp" { type Key = null | string | undefined // The `class` property represents an HTML class attribute string. - type ClassProp = string | Record | ClassProp[] + type ClassProp = false | string | Record | ClassProp[] // The `style` property represents inline CSS. type StyleProp = Record From c717670620994c5153337115f86a4f67a1a24712 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Tue, 18 Aug 2020 15:53:13 -0400 Subject: [PATCH 06/46] Refactor while making a couple convenience types. `Transition` and `StateWithEffects` (especially the latter) were created to make typing the return types of actions easier to do. The benefit they provide is best seen when working with actions that return more than just an updated state. As a bonus they allowed the type definitions here to be further streamlined. --- index.d.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index b3c0d8e91..3dfb51251 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,25 +2,26 @@ declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. type App = Readonly<{ - init: Initiator; + init: Transition; view: View; node: Node; subscriptions?: Subscription; middleware?: Middleware; }> - // Initially, a state is set with any effects to run, or an action is taken. - type Initiator - = State - | [State, ...EffectDescriptor[]] - | Action - - // A view builds a virtual DOM node representation of the application state. - type View = (state: State) => VDOM + // A transition is either a state transformation with any effects to run, or + // an action to take. + type Transition = State | StateWithEffects | Action // Application state is accessible in every view, action, and subscription. type State = S + // Transformed state can be paired with a list of effects to run. + type StateWithEffects = [State, ...EffectDescriptor[]] + + // A view builds a virtual DOM node representation of the application state. + type View = (state: State) => VDOM + // A subscription is a set of recurring effects. type Subscription = (state: State) => Subscriber[] @@ -38,14 +39,10 @@ declare module "hyperapp" { // A dispatched action handles an event in the context of the current state. type Dispatch = (action: Action, props?: Payload

) => void - // An action transforms existing state while possibly invoking effects and it - // can be wrapped by another action. + // An action transforms existing state and can be wrapped by another action. type Action = [Action, Payload

] - | ((state: State, props?: Payload

) - => State - | [State, ...EffectDescriptor[]] - | Action) + | ((state: State, props?: Payload

) => Transition) // A payload is data external to state that is given to a dispatched action. type Payload

= P From b12708990cfcd7e9b69d353ec6e442b1abb1dbff Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Tue, 18 Aug 2020 17:22:17 -0400 Subject: [PATCH 07/46] Allow effects to unsubscribe from subscriptions. --- index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 3dfb51251..65e02b339 100644 --- a/index.d.ts +++ b/index.d.ts @@ -52,7 +52,8 @@ declare module "hyperapp" { type EffectDescriptor = [Effect, EffectData] // An effect is where side effects and any additional dispatching occur. - type Effect = (dispatch: Dispatch, props?: EffectData) => void + // An effect used in a subscription should be able to unsubscribe. + type Effect = (dispatch: Dispatch, props?: EffectData) => void | Unsubscribe // An effect is generally given additional data. type EffectData = D From 3da5b09f26f3dcc9105a48cf88a26c8ac1afa6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Gl=C3=A4=C3=9Fle?= Date: Sun, 23 Aug 2020 15:18:43 +0200 Subject: [PATCH 08/46] Fix minor issues in reference.md --- docs/reference.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 8c2f433c8..82b589a5a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -24,7 +24,7 @@ Below is a consice recap of Hyperapp's core APIs and packages. It's geared towar ## h() ```js -h(type, props, ...children) +h(type, props, children) ``` Hyperscript function to create virtual DOM nodes (VNodes), which are used for [rendering](#rendering). @@ -33,7 +33,7 @@ A VNode is a simplified representation of an HTML element. It represents an elem - **type** - Name of the node, eg: div, h1, button, etc. - **props** - Object containing HTML or SVG attributes the DOM node will have and [special props](#special-props). -- **children** - Array of child VNodes. +- **children** - Array of child VNodes (optional). ```js import { h, text } from "hyperapp" @@ -322,7 +322,7 @@ const httpFx = (dispatch, props) => { // Do side effects fetch(props.url, props.options) .then((res) => res.json()) - .then((data) => dispatch(data)) // Optionnally dispatch an action + .then((data) => dispatch(props.action, data)) // Optionally dispatch an action } // Helper to easily create the effect tuple for the Http effect From 7231ece7dbb0c5646795eee9b98b2ef1dd4cb0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Gl=C3=A4=C3=9Fle?= Date: Sun, 23 Aug 2020 15:19:20 +0200 Subject: [PATCH 09/46] Fix some outdated text in tutorial.md --- docs/tutorial.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 50a2ff9c5..583495272 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -111,6 +111,7 @@ Create this html file: // -- RUN -- app({ + init: {}, node: document.getElementById("app"), view: () => h("h1", {}, [text("Hello "), h("i", {}, text("World!"))]), }) @@ -141,7 +142,7 @@ which _represent_ DOM nodes. The result of ```js -h("h1", {}, ["Hello ", h("i", {}, "World!")]) +h("h1", {}, [text("Hello "), h("i", {}, text("World!"))]) ``` is a virtual node, representing @@ -261,7 +262,7 @@ into this: ... h("p", {class: "title"}, emphasize("ocean", "The Ocean is Sinking" - )) + )), ... ``` @@ -473,7 +474,7 @@ ternary operator (`a ? b : c`). ```js const filterView = (props) => h("div", { class: "filter" }, [ - "Filter:", + text("Filter:"), props.editingFilter // <--- ? h("input", { type: "text", value: props.filter }) // <--- @@ -497,7 +498,7 @@ and update `filterView` again: ```js const filterView = (props) => h("div", { class: "filter" }, [ - "Filter:", + text("Filter:"), props.editingFilter ? h("input", { type: "text", value: props.filter }) @@ -523,7 +524,7 @@ Update `filterView` yet again: ```js const filterView = (props) => h("div", { class: "filter" }, [ - "Filter:", + text("Filter:"), props.editingFilter ? h("input", { @@ -629,7 +630,7 @@ const filterView = (props) => ? h("input", { type: "text", value: props.filter, - oninput: [SetFilter, (event) => event.target.value], // <---- + oninput: (state, event) => SetFilter(state, event.target.value), // <---- }) : h("span", { class: "filter-word" }, text(props.filter)), @@ -639,11 +640,6 @@ const filterView = (props) => ]) ``` -When we give a _function_ as the custom payload, Hyperapp considers it a _payload filter_ and passes the default -payload through it, providing the returned value as payload to the action. - -> Payload filters are also useful when you need a payload that is a combination of custom data and event data - If you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-2-5yv34) ## Effects From f33879995615684240f1276da60d2b2f7f96b1f1 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Wed, 26 Aug 2020 13:15:00 -0400 Subject: [PATCH 10/46] Clean up some code style --- index.d.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/index.d.ts b/index.d.ts index 65e02b339..4ef6a471c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,11 +2,11 @@ declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. type App = Readonly<{ - init: Transition; - view: View; - node: Node; - subscriptions?: Subscription; - middleware?: Middleware; + init: Transition + view: View + node: Node + subscriptions?: Subscription + middleware?: Middleware }> // A transition is either a state transformation with any effects to run, or @@ -62,22 +62,22 @@ declare module "hyperapp" { // A virtual DOM node represents an actual DOM element. type VDOM = { - readonly type: string; - readonly props: PropList; - readonly children: VNode[]; - node: MaybeNode; - readonly tag?: Tag; - readonly key: Key; - memo?: PropList; + readonly type: string + readonly props: PropList + readonly children: VNode[] + node: MaybeNode + readonly tag?: Tag + readonly key: Key + memo?: PropList } // Virtual DOM properties will often correspond to HTML attributes. type Prop = bigint | boolean | null | number | string | symbol | undefined | Function | ClassProp | StyleProp type PropList = Readonly // A key can uniquely associate a virtual DOM node with a certain DOM element. From 73175f22a7ac50a77637381edd2ca1de8f122a9d Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 3 Sep 2020 01:30:30 -0400 Subject: [PATCH 11/46] Adjust some code style. --- index.d.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4ef6a471c..c557b225c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,5 @@ +// These definitions are known to work for TypeScript 4.0. + declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. @@ -72,22 +74,22 @@ declare module "hyperapp" { } // Virtual DOM properties will often correspond to HTML attributes. - type Prop = bigint | boolean | null | number | string | symbol | undefined | Function | ClassProp | StyleProp + type Prop = bigint | boolean | number | string | symbol | null | undefined | Function | ClassProp | StyleProp type PropList = Readonly // A key can uniquely associate a virtual DOM node with a certain DOM element. - type Key = null | string | undefined + type Key = string | null | undefined // The `class` property represents an HTML class attribute string. type ClassProp = false | string | Record | ClassProp[] // The `style` property represents inline CSS. - type StyleProp = Record + type StyleProp = Record // A virtual node is a convenience layer over a virtual DOM node. type VNode = boolean | null | undefined | VDOM @@ -101,10 +103,7 @@ declare module "hyperapp" { // These are based on actual DOM node types: // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType - const enum VDOMNodeType { - SSR = 1, - Text = 3, - } + const enum VDOMNodeType { SSR = 1, Text = 3 } // --------------------------------------------------------------------------- @@ -119,5 +118,5 @@ declare module "hyperapp" { function memo(view: View, props: PropList): Partial // The `text` function creates a virtual DOM node representing plain text. - function text(value: string | number, node?: Node): VDOM + function text(value: number | string, node?: Node): VDOM } From e4d5919c198552e4526104cd41f1b471abda1835 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 3 Sep 2020 01:45:12 -0400 Subject: [PATCH 12/46] Simplify type variable usage. --- index.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index c557b225c..3ae14a027 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,7 +13,7 @@ declare module "hyperapp" { // A transition is either a state transformation with any effects to run, or // an action to take. - type Transition = State | StateWithEffects | Action + type Transition = State | StateWithEffects | Action

// Application state is accessible in every view, action, and subscription. type State = S @@ -39,12 +39,12 @@ declare module "hyperapp" { // --------------------------------------------------------------------------- // A dispatched action handles an event in the context of the current state. - type Dispatch = (action: Action, props?: Payload

) => void + type Dispatch =

(action: Action

, props?: Payload

) => void // An action transforms existing state and can be wrapped by another action. - type Action - = [Action, Payload

] - | ((state: State, props?: Payload

) => Transition) + type Action

+ = [Action

, Payload

] + | ((state: State, props?: Payload

) => Transition) // A payload is data external to state that is given to a dispatched action. type Payload

= P From e76dcfd67e247d883497dc213be28a5158fa75bc Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 3 Sep 2020 01:46:58 -0400 Subject: [PATCH 13/46] Allow effects to be asynchronous. --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 3ae14a027..75d59653f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -55,7 +55,7 @@ declare module "hyperapp" { // An effect is where side effects and any additional dispatching occur. // An effect used in a subscription should be able to unsubscribe. - type Effect = (dispatch: Dispatch, props?: EffectData) => void | Unsubscribe + type Effect = (dispatch: Dispatch, props?: EffectData) => void | Unsubscribe | Promise // An effect is generally given additional data. type EffectData = D From 7d00455c496c442ca240ad13f6706836047bfe4a Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 3 Sep 2020 01:52:34 -0400 Subject: [PATCH 14/46] Update to include definitions for events. --- index.d.ts | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 234 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 75d59653f..e2d14f72f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -75,13 +75,17 @@ declare module "hyperapp" { // Virtual DOM properties will often correspond to HTML attributes. type Prop = bigint | boolean | number | string | symbol | null | undefined | Function | ClassProp | StyleProp - type PropList = Readonly + // Actions are used as event handlers. + type EventActions = { [K in keyof EventsMap]?: Action } + type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } + // A key can uniquely associate a virtual DOM node with a certain DOM element. type Key = string | null | undefined @@ -119,4 +123,233 @@ declare module "hyperapp" { // The `text` function creates a virtual DOM node representing plain text. function text(value: number | string, node?: Node): VDOM + + // --------------------------------------------------------------------------- + + // Due to current limitations with TypeScript (which should get resolved in + // the future: https://github.com/microsoft/TypeScript/pull/40336), here is + // a collection of modified copies of relevant event maps from TypeScript's + // "lib.dom.d.ts" definition file to assist with defining `EventActions`: + + type OnElementEventMap = { + "onfullscreenchange": Event + "onfullscreenerror": Event + } + + type OnGlobalEventHandlersEventMap = { + "onabort": UIEvent + "onanimationcancel": AnimationEvent + "onanimationend": AnimationEvent + "onanimationiteration": AnimationEvent + "onanimationstart": AnimationEvent + "onauxclick": MouseEvent + "onblur": FocusEvent + "oncancel": Event + "oncanplay": Event + "oncanplaythrough": Event + "onchange": Event + "onclick": MouseEvent + "onclose": Event + "oncontextmenu": MouseEvent + "oncuechange": Event + "ondblclick": MouseEvent + "ondrag": DragEvent + "ondragend": DragEvent + "ondragenter": DragEvent + "ondragexit": Event + "ondragleave": DragEvent + "ondragover": DragEvent + "ondragstart": DragEvent + "ondrop": DragEvent + "ondurationchange": Event + "onemptied": Event + "onended": Event + "onerror": ErrorEvent + "onfocus": FocusEvent + "onfocusin": FocusEvent + "onfocusout": FocusEvent + "ongotpointercapture": PointerEvent + "oninput": Event + "oninvalid": Event + "onkeydown": KeyboardEvent + "onkeypress": KeyboardEvent + "onkeyup": KeyboardEvent + "onload": Event + "onloadeddata": Event + "onloadedmetadata": Event + "onloadstart": Event + "onlostpointercapture": PointerEvent + "onmousedown": MouseEvent + "onmouseenter": MouseEvent + "onmouseleave": MouseEvent + "onmousemove": MouseEvent + "onmouseout": MouseEvent + "onmouseover": MouseEvent + "onmouseup": MouseEvent + "onpause": Event + "onplay": Event + "onplaying": Event + "onpointercancel": PointerEvent + "onpointerdown": PointerEvent + "onpointerenter": PointerEvent + "onpointerleave": PointerEvent + "onpointermove": PointerEvent + "onpointerout": PointerEvent + "onpointerover": PointerEvent + "onpointerup": PointerEvent + "onprogress": ProgressEvent + "onratechange": Event + "onreset": Event + "onresize": UIEvent + "onscroll": Event + "onsecuritypolicyviolation": SecurityPolicyViolationEvent + "onseeked": Event + "onseeking": Event + "onselect": Event + "onselectionchange": Event + "onselectstart": Event + "onstalled": Event + "onsubmit": Event + "onsuspend": Event + "ontimeupdate": Event + "ontoggle": Event + "ontouchcancel": TouchEvent + "ontouchend": TouchEvent + "ontouchmove": TouchEvent + "ontouchstart": TouchEvent + "ontransitioncancel": TransitionEvent + "ontransitionend": TransitionEvent + "ontransitionrun": TransitionEvent + "ontransitionstart": TransitionEvent + "onvolumechange": Event + "onwaiting": Event + "onwheel": WheelEvent + } + + type OnDocumentAndElementEventHandlersEventMap = { + "oncopy": ClipboardEvent + "oncut": ClipboardEvent + "onpaste": ClipboardEvent + } + + type OnHTMLElementEventMap = OnElementEventMap & OnGlobalEventHandlersEventMap & OnDocumentAndElementEventHandlersEventMap + + type OnWindowEventHandlersEventMap = { + "onafterprint": Event + "onbeforeprint": Event + "onbeforeunload": BeforeUnloadEvent + "onhashchange": HashChangeEvent + "onlanguagechange": Event + "onmessage": MessageEvent + "onmessageerror": MessageEvent + "onoffline": Event + "ononline": Event + "onpagehide": PageTransitionEvent + "onpageshow": PageTransitionEvent + "onpopstate": PopStateEvent + "onrejectionhandled": PromiseRejectionEvent + "onstorage": StorageEvent + "onunhandledrejection": PromiseRejectionEvent + "onunload": Event + } + + type OnWindowEventMap = OnGlobalEventHandlersEventMap & OnWindowEventHandlersEventMap & { + "onabort": UIEvent + "onafterprint": Event + "onbeforeprint": Event + "onbeforeunload": BeforeUnloadEvent + "onblur": FocusEvent + "oncanplay": Event + "oncanplaythrough": Event + "onchange": Event + "onclick": MouseEvent + "oncompassneedscalibration": Event + "oncontextmenu": MouseEvent + "ondblclick": MouseEvent + "ondevicelight": DeviceLightEvent + "ondevicemotion": DeviceMotionEvent + "ondeviceorientation": DeviceOrientationEvent + "ondeviceorientationabsolute": DeviceOrientationEvent + "ondrag": DragEvent + "ondragend": DragEvent + "ondragenter": DragEvent + "ondragleave": DragEvent + "ondragover": DragEvent + "ondragstart": DragEvent + "ondrop": DragEvent + "ondurationchange": Event + "onemptied": Event + "onended": Event + "onerror": ErrorEvent + "onfocus": FocusEvent + "onhashchange": HashChangeEvent + "oninput": Event + "oninvalid": Event + "onkeydown": KeyboardEvent + "onkeypress": KeyboardEvent + "onkeyup": KeyboardEvent + "onload": Event + "onloadeddata": Event + "onloadedmetadata": Event + "onloadstart": Event + "onmessage": MessageEvent + "onmousedown": MouseEvent + "onmouseenter": MouseEvent + "onmouseleave": MouseEvent + "onmousemove": MouseEvent + "onmouseout": MouseEvent + "onmouseover": MouseEvent + "onmouseup": MouseEvent + "onmousewheel": Event + "onMSGestureChange": Event + "onMSGestureDoubleTap": Event + "onMSGestureEnd": Event + "onMSGestureHold": Event + "onMSGestureStart": Event + "onMSGestureTap": Event + "onMSInertiaStart": Event + "onMSPointerCancel": Event + "onMSPointerDown": Event + "onMSPointerEnter": Event + "onMSPointerLeave": Event + "onMSPointerMove": Event + "onMSPointerOut": Event + "onMSPointerOver": Event + "onMSPointerUp": Event + "onoffline": Event + "ononline": Event + "onorientationchange": Event + "onpagehide": PageTransitionEvent + "onpageshow": PageTransitionEvent + "onpause": Event + "onplay": Event + "onplaying": Event + "onpopstate": PopStateEvent + "onprogress": ProgressEvent + "onratechange": Event + "onreadystatechange": ProgressEvent + "onreset": Event + "onresize": UIEvent + "onscroll": Event + "onseeked": Event + "onseeking": Event + "onselect": Event + "onstalled": Event + "onstorage": StorageEvent + "onsubmit": Event + "onsuspend": Event + "ontimeupdate": Event + "onunload": Event + "onvolumechange": Event + "onvrdisplayactivate": Event + "onvrdisplayblur": Event + "onvrdisplayconnect": Event + "onvrdisplaydeactivate": Event + "onvrdisplaydisconnect": Event + "onvrdisplayfocus": Event + "onvrdisplaypointerrestricted": Event + "onvrdisplaypointerunrestricted": Event + "onvrdisplaypresentchange": Event + "onwaiting": Event + } } From ea603919410ae034b49ab4b024e3745f35b3528a Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 3 Sep 2020 20:34:33 -0400 Subject: [PATCH 15/46] Separate state transitions from action nesting. --- index.d.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index e2d14f72f..c3ba30572 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,17 +3,16 @@ declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. - type App = Readonly<{ - init: Transition + type App = Readonly<{ + init: Transition view: View node: Node subscriptions?: Subscription middleware?: Middleware }> - // A transition is either a state transformation with any effects to run, or - // an action to take. - type Transition = State | StateWithEffects | Action

+ // A transition is a state transformation with any effects to run. + type Transition = State | StateWithEffects // Application state is accessible in every view, action, and subscription. type State = S @@ -44,7 +43,7 @@ declare module "hyperapp" { // An action transforms existing state and can be wrapped by another action. type Action

= [Action

, Payload

] - | ((state: State, props?: Payload

) => Transition) + | ((state: State, props?: Payload

) => Transition | Action

) // A payload is data external to state that is given to a dispatched action. type Payload

= P @@ -113,7 +112,7 @@ declare module "hyperapp" { // The `app` function initiates a Hyperapp application. `app` along with effects // should be the only places you need to worry about side effects. - function app(props: App): Dispatch + function app(props: App): Dispatch // The `h` function builds a virtual DOM node. function h(type: string, props: PropList, children?: VNode | readonly VNode[]): VDOM From a5a99cb0f84d9208d4430b2709f891496eb3b2b7 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 5 Sep 2020 11:21:25 -0400 Subject: [PATCH 16/46] Only allow functions be used for event handling. --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index c3ba30572..6d3b91821 100644 --- a/index.d.ts +++ b/index.d.ts @@ -73,7 +73,7 @@ declare module "hyperapp" { } // Virtual DOM properties will often correspond to HTML attributes. - type Prop = bigint | boolean | number | string | symbol | null | undefined | Function | ClassProp | StyleProp + type Prop = bigint | boolean | number | string | symbol | null | undefined | ClassProp | StyleProp type PropList = Readonly Date: Mon, 14 Sep 2020 15:18:08 -0400 Subject: [PATCH 17/46] Overhaul type definitions and start adding tests. --- package.json | 10 +- index.d.ts => types/index.d.ts | 80 +++++++------- types/test/app.test.ts | 60 +++++++++++ types/test/h.test.ts | 192 +++++++++++++++++++++++++++++++++ types/test/memo.test.ts | 25 +++++ types/test/text.test.ts | 20 ++++ types/tsconfig.json | 18 ++++ types/tslint.json | 15 +++ 8 files changed, 381 insertions(+), 39 deletions(-) rename index.d.ts => types/index.d.ts (80%) create mode 100644 types/test/app.test.ts create mode 100644 types/test/h.test.ts create mode 100644 types/test/memo.test.ts create mode 100644 types/test/text.test.ts create mode 100644 types/tsconfig.json create mode 100644 types/tslint.json diff --git a/package.json b/package.json index 53c98a6c9..34428a172 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,15 @@ "minify": "terser ${prefix}index.js -o ${prefix}$out -mc --source-map url=$out.map --module", "create": "npm run build && git commit -am $msg && git tag -s $msg -m $msg && git push && git push --tags", "message": "echo ${pkg:+@$npm_package_name/$pkg@}$(node -p \"require('./${pkg:+pkg/$pkg/}package').version\")", - "release": "env msg=$(npm run -s message) npm run create && cd ./${pkg:+pkg/$pkg} && npm publish --access public" + "release": "env msg=$(npm run -s message) npm run create && cd ./${pkg:+pkg/$pkg} && npm publish --access public", + "dtslint": "dtslint types" }, "devDependencies": { "c8": "*", + "dtslint": "^4.0.0", + "terser": "*", "twist": "*", - "terser": "*" - } + "typescript": "^4.0.2" + }, + "types": "./types" } diff --git a/index.d.ts b/types/index.d.ts similarity index 80% rename from index.d.ts rename to types/index.d.ts index 6d3b91821..685aec303 100644 --- a/index.d.ts +++ b/types/index.d.ts @@ -1,88 +1,89 @@ -// These definitions are known to work for TypeScript 4.0. +// Minimum TypeScript Version: 3.7 declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. - type App = Readonly<{ - init: Transition - view: View + type App = Readonly<{ + init: Transition | Action + view: View node: Node - subscriptions?: Subscription - middleware?: Middleware + subscriptions?: Subscription + middleware?: Middleware }> // A transition is a state transformation with any effects to run. - type Transition = State | StateWithEffects + type Transition = State | StateWithEffects // Application state is accessible in every view, action, and subscription. type State = S // Transformed state can be paired with a list of effects to run. - type StateWithEffects = [State, ...EffectDescriptor[]] + type StateWithEffects = [State, ...EffectDescriptor[]] // A view builds a virtual DOM node representation of the application state. - type View = (state: State) => VDOM + type View = (state: State) => VDOM // A subscription is a set of recurring effects. - type Subscription = (state: State) => Subscriber[] + type Subscription = (state: State) => Subscriber[] // A subscriber reacts to subscription updates. - type Subscriber = boolean | void | Effect | Unsubscribe + type Subscriber = boolean | undefined | Effect | Unsubscribe // A subscriber ideally provides a function that cancels itself properly. type Unsubscribe = () => void // Middleware allows for custom processing during dispatching. - type Middleware = (dispatch: Dispatch) => Dispatch + type Middleware = (dispatch: Dispatch) => Dispatch // --------------------------------------------------------------------------- // A dispatched action handles an event in the context of the current state. - type Dispatch =

(action: Action

, props?: Payload

) => void + type Dispatch = (action: Action, props?: Payload

) => void // An action transforms existing state and can be wrapped by another action. - type Action

- = [Action

, Payload

] - | ((state: State, props?: Payload

) => Transition | Action

) + type Action + = [Action, Payload

] + | ((state: State, props?: Payload

) => Transition | Action) // A payload is data external to state that is given to a dispatched action. - type Payload

= P + type Payload

= P // An effect descriptor describes how an effect should be invoked. // A function that creates this is called an effect constructor. - type EffectDescriptor = [Effect, EffectData] + type EffectDescriptor = [Effect, EffectData] // An effect is where side effects and any additional dispatching occur. // An effect used in a subscription should be able to unsubscribe. - type Effect = (dispatch: Dispatch, props?: EffectData) => void | Unsubscribe | Promise + type Effect = + (dispatch: Dispatch, props?: EffectData) => + void | Unsubscribe | Promise // An effect is generally given additional data. - type EffectData = D + type EffectData = D // --------------------------------------------------------------------------- // A virtual DOM node represents an actual DOM element. - type VDOM = { + type VDOM = { readonly type: string - readonly props: PropList - readonly children: VNode[] + readonly props: PropList + readonly children: VNode[] node: MaybeNode - readonly tag?: Tag + readonly tag?: Tag readonly key: Key - memo?: PropList + memo?: PropList } // Virtual DOM properties will often correspond to HTML attributes. - type Prop = bigint | boolean | number | string | symbol | null | undefined | ClassProp | StyleProp - type PropList = Readonly = Readonly & { + [_: string]: unknown class?: ClassProp key?: Key style?: StyleProp }> // Actions are used as event handlers. - type EventActions = { [K in keyof EventsMap]?: Action } + type EventActions = { [K in keyof EventsMap]?: Action } type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } // A key can uniquely associate a virtual DOM node with a certain DOM element. @@ -95,14 +96,14 @@ declare module "hyperapp" { type StyleProp = Record // A virtual node is a convenience layer over a virtual DOM node. - type VNode = boolean | null | undefined | VDOM + type VNode = false | null | undefined | VDOM // Actual DOM nodes will be manipulated depending on how property patching goes. type MaybeNode = null | undefined | Node // A virtual DOM node's tag has metadata relevant to it. Virtual DOM nodes are // tagged by their type to assist rendering. - type Tag = VDOMNodeType | View + type Tag = VDOMNodeType | View // These are based on actual DOM node types: // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType @@ -112,16 +113,20 @@ declare module "hyperapp" { // The `app` function initiates a Hyperapp application. `app` along with effects // should be the only places you need to worry about side effects. - function app(props: App): Dispatch + function app(props: App): Dispatch // The `h` function builds a virtual DOM node. - function h(type: string, props: PropList, children?: VNode | readonly VNode[]): VDOM + function h( + type: string, + props: PropList, + children?: VNode | readonly VNode[], + ): VDOM // The `memo` function stores a view along with properties for it. - function memo(view: View, props: PropList): Partial + function memo(view: View, props: PropList): Partial> // The `text` function creates a virtual DOM node representing plain text. - function text(value: number | string, node?: Node): VDOM + function text(value: number | string, node?: Node): VDOM // --------------------------------------------------------------------------- @@ -231,7 +236,10 @@ declare module "hyperapp" { "onpaste": ClipboardEvent } - type OnHTMLElementEventMap = OnElementEventMap & OnGlobalEventHandlersEventMap & OnDocumentAndElementEventHandlersEventMap + type OnHTMLElementEventMap + = OnElementEventMap + & OnGlobalEventHandlersEventMap + & OnDocumentAndElementEventHandlersEventMap type OnWindowEventHandlersEventMap = { "onafterprint": Event diff --git a/types/test/app.test.ts b/types/test/app.test.ts new file mode 100644 index 000000000..3ac86835f --- /dev/null +++ b/types/test/app.test.ts @@ -0,0 +1,60 @@ +import { app, h, text } from "hyperapp" + +app() // $ExpectError +app(true) // $ExpectError +app(false) // $ExpectError +app(0) // $ExpectError +app(2424) // $ExpectError +app(-123) // $ExpectError +app(-Infinity) // $ExpectError +app(Infinity) // $ExpectError +app(NaN) // $ExpectError +app("") // $ExpectError +app("hi") // $ExpectError +app({}) // $ExpectError +app(new Set()) // $ExpectError +app([]) // $ExpectError +app(Symbol()) // $ExpectError +app(() => {}) // $ExpectError +app(null) // $ExpectError +app(undefined) // $ExpectError + +type Test = { bar?: number, foo: number } + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("p", {}, [text(state.bar ?? "")]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("div", {}, [ + text(state.foo), + text(state.bar ?? ""), + h("button", { + onclick: (state) => ({ ...state, bar: state.foo * 2 }) + }, [text("clicky")]), + ]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("div", {}, [ + text(state.foo), + text(state.bar ?? ""), + h("button", { + onclick: (state) => ({ ...state, bar: state.foo * 2 }), + onchange: (state, event) => { + if (!event) return state + const target = event.target as HTMLInputElement + return ({ ...state, bar: target.checked ? 20 : 10 }) + }, + }, [text("clicky")]), + ]), + node: document.body +}) diff --git a/types/test/h.test.ts b/types/test/h.test.ts new file mode 100644 index 000000000..afd6d5934 --- /dev/null +++ b/types/test/h.test.ts @@ -0,0 +1,192 @@ +import { h, text } from "hyperapp" + +h() // $ExpectError +h(true) // $ExpectError +h(false) // $ExpectError +h(0) // $ExpectError +h(2424) // $ExpectError +h(-123) // $ExpectError +h(-Infinity) // $ExpectError +h(Infinity) // $ExpectError +h(NaN) // $ExpectError +h("") // $ExpectError +h("hi") // $ExpectError +h({}) // $ExpectError +h(new Set()) // $ExpectError +h([]) // $ExpectError +h(Symbol()) // $ExpectError +h(() => {}) // $ExpectError +h(null) // $ExpectError +h(undefined) // $ExpectError + +h(true, {}) // $ExpectError +h(false, {}) // $ExpectError +h(0, {}) // $ExpectError +h(2424, {}) // $ExpectError +h(-123, {}) // $ExpectError +h(-Infinity, {}) // $ExpectError +h(Infinity, {}) // $ExpectError +h(NaN, {}) // $ExpectError +h("", {}) // $ExpectType VDOM +h("hi", {}) // $ExpectType VDOM +h({}, {}) // $ExpectError +h(new Set(), {}) // $ExpectError +h([], {}) // $ExpectError +h(Symbol(), {}) // $ExpectError +h(() => {}, {}) // $ExpectError +h(null, {}) // $ExpectError +h(undefined, {}) // $ExpectError + +h("p", true) // $ExpectError +h("p", false) // $ExpectError +h("p", 0) // $ExpectError +h("p", 2424) // $ExpectError +h("p", -123) // $ExpectError +h("p", -Infinity) // $ExpectError +h("p", Infinity) // $ExpectError +h("p", NaN) // $ExpectError +h("p", "") // $ExpectError +h("p", "hi") // $ExpectError +h("p", {}) // $ExpectType VDOM +h("p", new Set()) // $ExpectError +h("p", []) // $ExpectError +h("p", Symbol()) // $ExpectError +h("p", () => {}) // $ExpectError +h("p", null) // $ExpectError +h("p", undefined) // $ExpectError + +h("p", {}, true) // $ExpectError +h("p", {}, false) // $ExpectType VDOM +h("p", {}, 0) // $ExpectError +h("p", {}, 2424) // $ExpectError +h("p", {}, -123) // $ExpectError +h("p", {}, -Infinity) // $ExpectError +h("p", {}, Infinity) // $ExpectError +h("p", {}, NaN) // $ExpectError +h("p", {}, "") // $ExpectError +h("p", {}, "hi") // $ExpectError +h("p", {}, {}) // $ExpectError +h("p", {}, new Set()) // $ExpectError +h("p", {}, []) // $ExpectType VDOM +h("p", {}, Symbol()) // $ExpectError +h("p", {}, () => {}) // $ExpectError +h("p", {}, null) // $ExpectType VDOM +h("p", {}, undefined) // $ExpectType VDOM + +// ----------------------------------------------------------------------------- + +h("hr", { class: true }) // $ExpectError +h("hr", { class: false }) // $ExpectType VDOM +h("hr", { class: 0 }) // $ExpectError +h("hr", { class: 2424 }) // $ExpectError +h("hr", { class: -123 }) // $ExpectError +h("hr", { class: -Infinity }) // $ExpectError +h("hr", { class: Infinity }) // $ExpectError +h("hr", { class: NaN }) // $ExpectError +h("hr", { class: "" }) // $ExpectType VDOM +h("hr", { class: "hi" }) // $ExpectType VDOM +h("hr", { class: {} }) // $ExpectType VDOM +h("hr", { class: new Set() }) // $ExpectError +h("hr", { class: [] }) // $ExpectType VDOM +h("hr", { class: Symbol() }) // $ExpectError +h("hr", { class: () => {} }) // $ExpectError +h("hr", { class: null }) // $ExpectError +h("hr", { class: undefined }) // $ExpectType VDOM + +h("hr", { class: { a: true } }) // $ExpectType VDOM +h("hr", { class: { a: false } }) // $ExpectType VDOM +h("hr", { class: { a: 0 } }) // $ExpectError +h("hr", { class: { a: 2424 } }) // $ExpectError +h("hr", { class: { a: -123 } }) // $ExpectError +h("hr", { class: { a: -Infinity } }) // $ExpectError +h("hr", { class: { a: Infinity } }) // $ExpectError +h("hr", { class: { a: NaN } }) // $ExpectError +h("hr", { class: { a: "" } }) // $ExpectError +h("hr", { class: { a: "hi" } }) // $ExpectError +h("hr", { class: { a: {} } }) // $ExpectError +h("hr", { class: { a: new Set() } }) // $ExpectError +h("hr", { class: { a: [] } }) // $ExpectError +h("hr", { class: { a: Symbol() } }) // $ExpectError +h("hr", { class: { a: () => {} } }) // $ExpectError +h("hr", { class: { a: null } }) // $ExpectError +h("hr", { class: { a: undefined } }) // $ExpectError + +h("hr", { class: [true] }) // $ExpectError +h("hr", { class: [false] }) // $ExpectType VDOM +h("hr", { class: [0] }) // $ExpectError +h("hr", { class: [2424] }) // $ExpectError +h("hr", { class: [-123] }) // $ExpectError +h("hr", { class: [-Infinity] }) // $ExpectError +h("hr", { class: [Infinity] }) // $ExpectError +h("hr", { class: [NaN] }) // $ExpectError +h("hr", { class: [""] }) // $ExpectType VDOM +h("hr", { class: ["hi"] }) // $ExpectType VDOM +h("hr", { class: [{}] }) // $ExpectType VDOM +h("hr", { class: [new Set()] }) // $ExpectError +h("hr", { class: [[]] }) // $ExpectType VDOM +h("hr", { class: [Symbol()] }) // $ExpectError +h("hr", { class: [() => {}] }) // $ExpectError +h("hr", { class: [null] }) // $ExpectError +h("hr", { class: [undefined] }) // $ExpectError + +h("hr", { class: [{}] }) // $ExpectType VDOM +h("hr", { class: [{ a: true }] }) // $ExpectType VDOM +h("hr", { class: [{ a: false }] }) // $ExpectType VDOM + +h("hr", { class: [false && "foo"] }) // $ExpectType VDOM + +// ----------------------------------------------------------------------------- + +// $ExpectType VDOM +h("a", { id: "unique" }, [h("br", {})]) + +// $ExpectType VDOM +h("a", { onclick: (state) => state }, [h("br", {})]) + +// $ExpectError +h("a", { onclick: (state) => ({ ...state }) }, [h("br", {})]) + +// $ExpectType VDOM +h("a", { onclick: (state) => [state] }, [h("br", {})]) + +// $ExpectError +h("a", { onclick: (state) => [{ ...state }] }, [h("br", {})]) + +// $ExpectType VDOM +h("a", { onclick: ((state: number) => state * 2) }, [h("br", {})]) + +// ----------------------------------------------------------------------------- + +// $ExpectType VDOM +h("a", { onclick: (state) => state * 2 }, [h("br", {})]) + +type Test = { bar?: number, foo: number } + +// $ExpectType VDOM +h("button", { + onclick: (state) => ({ ...state, bar: state.foo * 2 }) +}, [text("clicky")]) + +// ----------------------------------------------------------------------------- + +h("p", {}, [true]) // $ExpectError +h("p", {}, [false]) // $ExpectType VDOM +h("p", {}, [0]) // $ExpectError +h("p", {}, [2424]) // $ExpectError +h("p", {}, [-123]) // $ExpectError +h("p", {}, [-Infinity]) // $ExpectError +h("p", {}, [Infinity]) // $ExpectError +h("p", {}, [NaN]) // $ExpectError +h("p", {}, [""]) // $ExpectError +h("p", {}, ["hi"]) // $ExpectError +h("p", {}, [{}]) // $ExpectError +h("p", {}, [new Set()]) // $ExpectError +h("p", {}, [[]]) // $ExpectError +h("p", {}, [Symbol()]) // $ExpectError +h("p", {}, [() => {}]) // $ExpectError +h("p", {}, [null]) // $ExpectType VDOM +h("p", {}, [undefined]) // $ExpectType VDOM + +h("p", {}, h("br", {})) // $ExpectType VDOM +h("p", {}, [h("br", {})]) // $ExpectType VDOM +h("p", {}, [text("hello")]) // $ExpectType VDOM diff --git a/types/test/memo.test.ts b/types/test/memo.test.ts new file mode 100644 index 000000000..8752e5288 --- /dev/null +++ b/types/test/memo.test.ts @@ -0,0 +1,25 @@ +import { memo } from "hyperapp" + +memo() // $ExpectError +memo(true) // $ExpectError +memo(false) // $ExpectError +memo(0) // $ExpectError +memo(2424) // $ExpectError +memo(-123) // $ExpectError +memo(-Infinity) // $ExpectError +memo(Infinity) // $ExpectError +memo(NaN) // $ExpectError +memo("") // $ExpectError +memo("hi") // $ExpectError +memo({}) // $ExpectError +memo(new Set()) // $ExpectError +memo([]) // $ExpectError +memo(Symbol()) // $ExpectError +memo(() => {}) // $ExpectError +memo(null) // $ExpectError +memo(undefined) // $ExpectError + +// TODO: +// export var memo = (tag, memo) => ({ tag, memo }) +// // The `memo` function stores a view along with properties for it. +// function memo(view: View, props: PropList): Partial> diff --git a/types/test/text.test.ts b/types/test/text.test.ts new file mode 100644 index 000000000..e345f192e --- /dev/null +++ b/types/test/text.test.ts @@ -0,0 +1,20 @@ +import { text } from "hyperapp" + +text() // $ExpectError +text(true) // $ExpectError +text(false) // $ExpectError +text(0) // $ExpectType VDOM +text(2424) // $ExpectType VDOM +text(-123) // $ExpectType VDOM +text(-Infinity) // $ExpectType VDOM +text(Infinity) // $ExpectType VDOM +text(NaN) // $ExpectType VDOM +text("") // $ExpectType VDOM +text("hi") // $ExpectType VDOM +text({}) // $ExpectError +text(new Set()) // $ExpectError +text([]) // $ExpectError +text(Symbol()) // $ExpectError +text(() => {}) // $ExpectError +text(null) // $ExpectError +text(undefined) // $ExpectError diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 000000000..729ad3384 --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,18 @@ +// https://www.typescriptlang.org/tsconfig +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +// https://www.typescriptlang.org/docs/handbook/compiler-options.html + +{ + "compilerOptions": { + "allowJs": true, + "alwaysStrict": true, + "baseUrl": ".", + "lib": ["DOM", "ESNext"], + "module": "ESNext", + "moduleResolution": "node", + "noEmit": true, + "paths": { "hyperapp": ["."] }, + "strict": true, + "target": "ESNext" + } +} diff --git a/types/tslint.json b/types/tslint.json new file mode 100644 index 000000000..d0b44879d --- /dev/null +++ b/types/tslint.json @@ -0,0 +1,15 @@ +{ + "//": "https://palantir.github.io/tslint/rules/", + "///": "https://github.com/microsoft/dtslint/tree/master/docs", + "extends": "dtslint/dtslint.json", + "rules": { + "array-type": [true, "array"], + "indent": [true, "spaces"], + "interface-over-type-literal": false, + "semicolon": false, + + "no-const-enum": false, + "no-single-declare-module": false, + "no-unnecessary-generics": false + } +} From 839367bb9dceee92cf7ea5ea778b5bb639e7ac21 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 14 Sep 2020 21:11:28 -0400 Subject: [PATCH 18/46] Tweak code style. --- types/index.d.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 685aec303..2cb52da48 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,13 +3,14 @@ declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. - type App = Readonly<{ - init: Transition | Action - view: View - node: Node - subscriptions?: Subscription - middleware?: Middleware - }> + type App + = Readonly<{ + init: Transition | Action + view: View + node: Node + subscriptions?: Subscription + middleware?: Middleware + }> // A transition is a state transformation with any effects to run. type Transition = State | StateWithEffects @@ -54,9 +55,9 @@ declare module "hyperapp" { // An effect is where side effects and any additional dispatching occur. // An effect used in a subscription should be able to unsubscribe. - type Effect = - (dispatch: Dispatch, props?: EffectData) => - void | Unsubscribe | Promise + type Effect + = (dispatch: Dispatch, props?: EffectData) => + void | Unsubscribe | Promise // An effect is generally given additional data. type EffectData = D @@ -75,12 +76,13 @@ declare module "hyperapp" { } // Virtual DOM properties will often correspond to HTML attributes. - type PropList = Readonly & { - [_: string]: unknown - class?: ClassProp - key?: Key - style?: StyleProp - }> + type PropList + = Readonly & { + [_: string]: unknown + class?: ClassProp + key?: Key + style?: StyleProp + }> // Actions are used as event handlers. type EventActions = { [K in keyof EventsMap]?: Action } From 5b3e6470f1aaa94376fac52b64672f5c9a415195 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 14 Sep 2020 21:24:30 -0400 Subject: [PATCH 19/46] Refine `style` property and update tests. --- types/index.d.ts | 5 +- types/test/h.test.ts | 177 ++++++++++++++++++++++++++++--------------- 2 files changed, 120 insertions(+), 62 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 2cb52da48..97e46b8e0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -95,7 +95,10 @@ declare module "hyperapp" { type ClassProp = false | string | Record | ClassProp[] // The `style` property represents inline CSS. - type StyleProp = Record + type StyleProp + = { [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null } + // For some reason we need this to prevent `style` from being a string. + & { [index: number]: never } // A virtual node is a convenience layer over a virtual DOM node. type VNode = false | null | undefined | VDOM diff --git a/types/test/h.test.ts b/types/test/h.test.ts index afd6d5934..e037152b1 100644 --- a/types/test/h.test.ts +++ b/types/test/h.test.ts @@ -75,65 +75,122 @@ h("p", {}, undefined) // $ExpectType VDOM // ----------------------------------------------------------------------------- -h("hr", { class: true }) // $ExpectError -h("hr", { class: false }) // $ExpectType VDOM -h("hr", { class: 0 }) // $ExpectError -h("hr", { class: 2424 }) // $ExpectError -h("hr", { class: -123 }) // $ExpectError -h("hr", { class: -Infinity }) // $ExpectError -h("hr", { class: Infinity }) // $ExpectError -h("hr", { class: NaN }) // $ExpectError -h("hr", { class: "" }) // $ExpectType VDOM -h("hr", { class: "hi" }) // $ExpectType VDOM -h("hr", { class: {} }) // $ExpectType VDOM -h("hr", { class: new Set() }) // $ExpectError -h("hr", { class: [] }) // $ExpectType VDOM -h("hr", { class: Symbol() }) // $ExpectError -h("hr", { class: () => {} }) // $ExpectError -h("hr", { class: null }) // $ExpectError -h("hr", { class: undefined }) // $ExpectType VDOM - -h("hr", { class: { a: true } }) // $ExpectType VDOM -h("hr", { class: { a: false } }) // $ExpectType VDOM -h("hr", { class: { a: 0 } }) // $ExpectError -h("hr", { class: { a: 2424 } }) // $ExpectError -h("hr", { class: { a: -123 } }) // $ExpectError -h("hr", { class: { a: -Infinity } }) // $ExpectError -h("hr", { class: { a: Infinity } }) // $ExpectError -h("hr", { class: { a: NaN } }) // $ExpectError -h("hr", { class: { a: "" } }) // $ExpectError -h("hr", { class: { a: "hi" } }) // $ExpectError -h("hr", { class: { a: {} } }) // $ExpectError -h("hr", { class: { a: new Set() } }) // $ExpectError -h("hr", { class: { a: [] } }) // $ExpectError -h("hr", { class: { a: Symbol() } }) // $ExpectError -h("hr", { class: { a: () => {} } }) // $ExpectError -h("hr", { class: { a: null } }) // $ExpectError -h("hr", { class: { a: undefined } }) // $ExpectError - -h("hr", { class: [true] }) // $ExpectError -h("hr", { class: [false] }) // $ExpectType VDOM -h("hr", { class: [0] }) // $ExpectError -h("hr", { class: [2424] }) // $ExpectError -h("hr", { class: [-123] }) // $ExpectError -h("hr", { class: [-Infinity] }) // $ExpectError -h("hr", { class: [Infinity] }) // $ExpectError -h("hr", { class: [NaN] }) // $ExpectError -h("hr", { class: [""] }) // $ExpectType VDOM -h("hr", { class: ["hi"] }) // $ExpectType VDOM -h("hr", { class: [{}] }) // $ExpectType VDOM -h("hr", { class: [new Set()] }) // $ExpectError -h("hr", { class: [[]] }) // $ExpectType VDOM -h("hr", { class: [Symbol()] }) // $ExpectError -h("hr", { class: [() => {}] }) // $ExpectError -h("hr", { class: [null] }) // $ExpectError -h("hr", { class: [undefined] }) // $ExpectError - -h("hr", { class: [{}] }) // $ExpectType VDOM -h("hr", { class: [{ a: true }] }) // $ExpectType VDOM -h("hr", { class: [{ a: false }] }) // $ExpectType VDOM - -h("hr", { class: [false && "foo"] }) // $ExpectType VDOM +h("p", { class: true }) // $ExpectError +h("p", { class: false }) // $ExpectType VDOM +h("p", { class: 0 }) // $ExpectError +h("p", { class: 2424 }) // $ExpectError +h("p", { class: -123 }) // $ExpectError +h("p", { class: -Infinity }) // $ExpectError +h("p", { class: Infinity }) // $ExpectError +h("p", { class: NaN }) // $ExpectError +h("p", { class: "" }) // $ExpectType VDOM +h("p", { class: "hi" }) // $ExpectType VDOM +h("p", { class: {} }) // $ExpectType VDOM +h("p", { class: new Set() }) // $ExpectError +h("p", { class: [] }) // $ExpectType VDOM +h("p", { class: Symbol() }) // $ExpectError +h("p", { class: () => {} }) // $ExpectError +h("p", { class: null }) // $ExpectError +h("p", { class: undefined }) // $ExpectType VDOM + +h("p", { class: { a: true } }) // $ExpectType VDOM +h("p", { class: { a: false } }) // $ExpectType VDOM +h("p", { class: { a: 0 } }) // $ExpectError +h("p", { class: { a: 2424 } }) // $ExpectError +h("p", { class: { a: -123 } }) // $ExpectError +h("p", { class: { a: -Infinity } }) // $ExpectError +h("p", { class: { a: Infinity } }) // $ExpectError +h("p", { class: { a: NaN } }) // $ExpectError +h("p", { class: { a: "" } }) // $ExpectError +h("p", { class: { a: "hi" } }) // $ExpectError +h("p", { class: { a: {} } }) // $ExpectError +h("p", { class: { a: new Set() } }) // $ExpectError +h("p", { class: { a: [] } }) // $ExpectError +h("p", { class: { a: Symbol() } }) // $ExpectError +h("p", { class: { a: () => {} } }) // $ExpectError +h("p", { class: { a: null } }) // $ExpectError +h("p", { class: { a: undefined } }) // $ExpectError + +h("p", { class: [true] }) // $ExpectError +h("p", { class: [false] }) // $ExpectType VDOM +h("p", { class: [0] }) // $ExpectError +h("p", { class: [2424] }) // $ExpectError +h("p", { class: [-123] }) // $ExpectError +h("p", { class: [-Infinity] }) // $ExpectError +h("p", { class: [Infinity] }) // $ExpectError +h("p", { class: [NaN] }) // $ExpectError +h("p", { class: [""] }) // $ExpectType VDOM +h("p", { class: ["hi"] }) // $ExpectType VDOM +h("p", { class: [{}] }) // $ExpectType VDOM +h("p", { class: [new Set()] }) // $ExpectError +h("p", { class: [[]] }) // $ExpectType VDOM +h("p", { class: [Symbol()] }) // $ExpectError +h("p", { class: [() => {}] }) // $ExpectError +h("p", { class: [null] }) // $ExpectError +h("p", { class: [undefined] }) // $ExpectError + +h("p", { class: [{}] }) // $ExpectType VDOM +h("p", { class: [{ a: true }] }) // $ExpectType VDOM +h("p", { class: [{ a: false }] }) // $ExpectType VDOM + +h("p", { class: [false && "foo"] }) // $ExpectType VDOM + +// ----------------------------------------------------------------------------- + +h("p", { style: true }) // $ExpectError +h("p", { style: false }) // $ExpectError +h("p", { style: 0 }) // $ExpectError +h("p", { style: 2424 }) // $ExpectError +h("p", { style: -123 }) // $ExpectError +h("p", { style: -Infinity }) // $ExpectError +h("p", { style: Infinity }) // $ExpectError +h("p", { style: NaN }) // $ExpectError +h("p", { style: "" }) // $ExpectError +h("p", { style: "hi" }) // $ExpectError +h("p", { style: {} }) // $ExpectType VDOM +h("p", { style: new Set() }) // $ExpectError +h("p", { style: [] }) // $ExpectError +h("p", { style: Symbol() }) // $ExpectError +h("p", { style: () => {} }) // $ExpectError +h("p", { style: null }) // $ExpectError +h("p", { style: undefined }) // $ExpectType VDOM + +h("p", { style: { color: true } }) // $ExpectError +h("p", { style: { color: false } }) // $ExpectError +h("p", { style: { color: 0 } }) // $ExpectError +h("p", { style: { color: 2424 } }) // $ExpectError +h("p", { style: { color: -123 } }) // $ExpectError +h("p", { style: { color: -Infinity } }) // $ExpectError +h("p", { style: { color: Infinity } }) // $ExpectError +h("p", { style: { color: NaN } }) // $ExpectError +h("p", { style: { color: "" } }) // $ExpectType VDOM +h("p", { style: { color: "red" } }) // $ExpectType VDOM +h("p", { style: { color: {} } }) // $ExpectError +h("p", { style: { color: new Set() } }) // $ExpectError +h("p", { style: { color: [] } }) // $ExpectError +h("p", { style: { color: Symbol() } }) // $ExpectError +h("p", { style: { color: () => {} } }) // $ExpectError +h("p", { style: { color: null } }) // $ExpectType VDOM +h("p", { style: { color: undefined } }) // $ExpectType VDOM +h("p", { style: { clor: null } }) // $ExpectError + +h("p", { style: [true] }) // $ExpectError +h("p", { style: [false] }) // $ExpectError +h("p", { style: [0] }) // $ExpectError +h("p", { style: [2424] }) // $ExpectError +h("p", { style: [-123] }) // $ExpectError +h("p", { style: [-Infinity] }) // $ExpectError +h("p", { style: [Infinity] }) // $ExpectError +h("p", { style: [NaN] }) // $ExpectError +h("p", { style: [""] }) // $ExpectError +h("p", { style: ["hi"] }) // $ExpectError +h("p", { style: [{}] }) // $ExpectError +h("p", { style: [new Set()] }) // $ExpectError +h("p", { style: [[]] }) // $ExpectError +h("p", { style: [Symbol()] }) // $ExpectError +h("p", { style: [() => {}] }) // $ExpectError +h("p", { style: [null] }) // $ExpectError +h("p", { style: [undefined] }) // $ExpectError // ----------------------------------------------------------------------------- @@ -155,8 +212,6 @@ h("a", { onclick: (state) => [{ ...state }] }, [h("br", {})]) // $ExpectType VDOM h("a", { onclick: ((state: number) => state * 2) }, [h("br", {})]) -// ----------------------------------------------------------------------------- - // $ExpectType VDOM h("a", { onclick: (state) => state * 2 }, [h("br", {})]) From 2b17454586fa5ba551c32de45ad3236510e6452a Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Tue, 15 Sep 2020 16:16:38 -0400 Subject: [PATCH 20/46] Start adding effect type tests. --- types/test/effects.test.ts | 13 +++++++++++++ types/test/h.test.ts | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 types/test/effects.test.ts diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts new file mode 100644 index 000000000..628b2fa56 --- /dev/null +++ b/types/test/effects.test.ts @@ -0,0 +1,13 @@ +import { Effect, EffectData, EffectDescriptor } from "hyperapp" + +// $ExpectType Effect +const runSayHi = ((dispatch, data) => { + console.log(data) + dispatch((state, x) => state + x, data) +}) as Effect + +const sayHi = (x: EffectData): EffectDescriptor => + [runSayHi, x] + +// $ExpectType EffectDescriptor +sayHi("hi") diff --git a/types/test/h.test.ts b/types/test/h.test.ts index e037152b1..835bfc49d 100644 --- a/types/test/h.test.ts +++ b/types/test/h.test.ts @@ -215,6 +215,8 @@ h("a", { onclick: ((state: number) => state * 2) }, [h("br", {})]) // $ExpectType VDOM h("a", { onclick: (state) => state * 2 }, [h("br", {})]) +// ----------------------------------------------------------------------------- + type Test = { bar?: number, foo: number } // $ExpectType VDOM From a964fdfa4f1399eef77440bd00edf3f7162c3f0c Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Tue, 15 Sep 2020 16:53:40 -0400 Subject: [PATCH 21/46] Add type tests for async effects. --- types/test/effects.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts index 628b2fa56..fb262247d 100644 --- a/types/test/effects.test.ts +++ b/types/test/effects.test.ts @@ -11,3 +11,21 @@ const sayHi = (x: EffectData): EffectDescriptor // $ExpectType EffectDescriptor sayHi("hi") + +// ----------------------------------------------------------------------------- + +// Credit: https://gist.github.com/eteeselink/81314282c95cd692ea1d +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +// $ExpectType Effect +const runSayHiEventually = (async (dispatch, data) => { + await delay (5000) + console.log(data) + window.requestAnimationFrame(() => dispatch((state, x) => state + x, data)) +}) as Effect + +const sayHiEventually = (x: EffectData): EffectDescriptor => + [runSayHiEventually, x] + +// $ExpectType EffectDescriptor +sayHiEventually("hi") From 447c14176d51a3f8df4c30c8afc96c74768b4eeb Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Wed, 16 Sep 2020 02:16:13 -0400 Subject: [PATCH 22/46] Add type tests for middleware. --- types/test/app.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/types/test/app.test.ts b/types/test/app.test.ts index 3ac86835f..032ac2f9c 100644 --- a/types/test/app.test.ts +++ b/types/test/app.test.ts @@ -19,6 +19,8 @@ app(() => {}) // $ExpectError app(null) // $ExpectError app(undefined) // $ExpectError +// ----------------------------------------------------------------------------- + type Test = { bar?: number, foo: number } // $ExpectType Dispatch @@ -58,3 +60,25 @@ app({ ]), node: document.body }) + +// ----------------------------------------------------------------------------- + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("p", {}, [text(state.foo)]), + node: document.body, + middleware: (dispatch) => dispatch +}) + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("p", {}, [text(state.foo)]), + node: document.body, + middleware: (dispatch) => (action, props) => { + console.log(action) + console.log(props) + dispatch(action, props) + } +}) From b0c3b60560c7ed7018d1580836db48a165f4248d Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Wed, 16 Sep 2020 02:23:01 -0400 Subject: [PATCH 23/46] Tweak type tests for effects. --- types/test/effects.test.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts index fb262247d..54386ce84 100644 --- a/types/test/effects.test.ts +++ b/types/test/effects.test.ts @@ -1,16 +1,21 @@ -import { Effect, EffectData, EffectDescriptor } from "hyperapp" +import { Effect, EffectDescriptor } from "hyperapp" // $ExpectType Effect -const runSayHi = ((dispatch, data) => { +const runEcho = ((dispatch, data) => { console.log(data) dispatch((state, x) => state + x, data) }) as Effect -const sayHi = (x: EffectData): EffectDescriptor => - [runSayHi, x] +const echo = (x: string): EffectDescriptor => + [runEcho, x] // $ExpectType EffectDescriptor -sayHi("hi") +echo("hi") + +// ----------------------------------------------------------------------------- + +// TODO: +// => Unsubscribe // ----------------------------------------------------------------------------- @@ -18,14 +23,19 @@ sayHi("hi") const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) // $ExpectType Effect -const runSayHiEventually = (async (dispatch, data) => { - await delay (5000) +const runEchoEventually = (async (dispatch, data) => { + await delay(5000) console.log(data) window.requestAnimationFrame(() => dispatch((state, x) => state + x, data)) }) as Effect -const sayHiEventually = (x: EffectData): EffectDescriptor => - [runSayHiEventually, x] +const sayEchoEventually = (x: string): EffectDescriptor => + [runEchoEventually, x] // $ExpectType EffectDescriptor -sayHiEventually("hi") +sayEchoEventually("hi") + +// ----------------------------------------------------------------------------- + +// TODO: +// => Promise From 6e53553835443c0024901298a55898fbf868ea53 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Wed, 16 Sep 2020 02:23:58 -0400 Subject: [PATCH 24/46] Add type tests for actions. --- types/test/actions.test.ts | 101 +++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 types/test/actions.test.ts diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts new file mode 100644 index 000000000..f84769096 --- /dev/null +++ b/types/test/actions.test.ts @@ -0,0 +1,101 @@ +import { + Dispatch, EffectData, EffectDescriptor, State, Transition, + app, h, text, +} from "hyperapp" + +type Test = { x: number } + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state) => ({ ...state, x: state.x * 2 }) + }, [text(state.x)]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state) => [{ ...state, x: state.x * 2 }] + }, [text(state.x)]), + node: document.body +}) + +// ----------------------------------------------------------------------------- + +const runJustEcho = (_dispatch: Dispatch, data: EffectData) => { + console.log(data) +} + +const justEcho =

(x: string): EffectDescriptor => + [runJustEcho, x] + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state) => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + }, [text(state.x)]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state: State): Transition => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + }, [text(state.x)]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onkeypress: (state: State): Transition => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + }, [text(state.x)]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state) => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + onkeypress: (state) => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + }, [text(state.x)]), + node: document.body +}) + +// $ExpectType Dispatch +app({ + init: { x: 2 }, + view: (state) => h("button", { + onclick: (state: State): Transition => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + onkeypress: (state: State): Transition => [ + { ...state, x: state.x * 2 }, + justEcho("hi"), + ], + }, [text(state.x)]), + node: document.body +}) From 105bea1200a08f7e6722b2862412e764052f980a Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 17 Sep 2020 10:50:42 -0400 Subject: [PATCH 25/46] Refactor `Action`. --- types/index.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 97e46b8e0..9deb76b62 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -41,10 +41,13 @@ declare module "hyperapp" { // A dispatched action handles an event in the context of the current state. type Dispatch = (action: Action, props?: Payload

) => void - // An action transforms existing state and can be wrapped by another action. + // An action transforms existing state and/or wraps another action. type Action - = [Action, Payload

] - | ((state: State, props?: Payload

) => Transition | Action) + = ((state: State, props?: Payload

) => Transition | Action) + | ActionDescriptor + + // An action descriptor describes an action and any payload for it. + type ActionDescriptor = [Action, Payload

] // A payload is data external to state that is given to a dispatched action. type Payload

= P From d548400ea50f1e5d12a2778ea6ada58e822c5ba7 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 17 Sep 2020 14:24:03 -0400 Subject: [PATCH 26/46] Reorder types. --- types/index.d.ts | 96 ++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9deb76b62..6c6e8a797 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,25 @@ // Minimum TypeScript Version: 3.7 declare module "hyperapp" { + // The `app` function initiates a Hyperapp application. `app` along with + // effects are the only places where side effects are allowed. + function app(props: App): Dispatch + + // The `h` function builds a virtual DOM node. + function h( + type: string, + props: PropList, + children?: VNode | readonly VNode[], + ): VDOM + + // The `memo` function stores a view along with properties for it. + function memo(view: View, props: PropList): Partial> + + // The `text` function creates a virtual DOM node representing plain text. + function text(value: number | string, node?: Node): VDOM + + // --------------------------------------------------------------------------- + // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. type App @@ -12,15 +31,6 @@ declare module "hyperapp" { middleware?: Middleware }> - // A transition is a state transformation with any effects to run. - type Transition = State | StateWithEffects - - // Application state is accessible in every view, action, and subscription. - type State = S - - // Transformed state can be paired with a list of effects to run. - type StateWithEffects = [State, ...EffectDescriptor[]] - // A view builds a virtual DOM node representation of the application state. type View = (state: State) => VDOM @@ -52,6 +62,15 @@ declare module "hyperapp" { // A payload is data external to state that is given to a dispatched action. type Payload

= P + // A transition is a state transformation with any effects to run. + type Transition = State | StateWithEffects + + // Application state is accessible in every view, action, and subscription. + type State = S + + // Transformed state can be paired with a list of effects to run. + type StateWithEffects = [State, ...EffectDescriptor[]] + // An effect descriptor describes how an effect should be invoked. // A function that creates this is called an effect constructor. type EffectDescriptor = [Effect, EffectData] @@ -78,37 +97,15 @@ declare module "hyperapp" { memo?: PropList } - // Virtual DOM properties will often correspond to HTML attributes. - type PropList - = Readonly & { - [_: string]: unknown - class?: ClassProp - key?: Key - style?: StyleProp - }> - - // Actions are used as event handlers. - type EventActions = { [K in keyof EventsMap]?: Action } - type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } - // A key can uniquely associate a virtual DOM node with a certain DOM element. type Key = string | null | undefined - // The `class` property represents an HTML class attribute string. - type ClassProp = false | string | Record | ClassProp[] - - // The `style` property represents inline CSS. - type StyleProp - = { [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null } - // For some reason we need this to prevent `style` from being a string. - & { [index: number]: never } + // Actual DOM nodes will be manipulated depending on how property patching goes. + type MaybeNode = null | undefined | Node // A virtual node is a convenience layer over a virtual DOM node. type VNode = false | null | undefined | VDOM - // Actual DOM nodes will be manipulated depending on how property patching goes. - type MaybeNode = null | undefined | Node - // A virtual DOM node's tag has metadata relevant to it. Virtual DOM nodes are // tagged by their type to assist rendering. type Tag = VDOMNodeType | View @@ -117,24 +114,27 @@ declare module "hyperapp" { // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType const enum VDOMNodeType { SSR = 1, Text = 3 } - // --------------------------------------------------------------------------- - - // The `app` function initiates a Hyperapp application. `app` along with effects - // should be the only places you need to worry about side effects. - function app(props: App): Dispatch + // Virtual DOM properties will often correspond to HTML attributes. + type PropList + = Readonly & { + [_: string]: unknown + class?: ClassProp + key?: Key + style?: StyleProp + }> - // The `h` function builds a virtual DOM node. - function h( - type: string, - props: PropList, - children?: VNode | readonly VNode[], - ): VDOM + // The `class` property represents an HTML class attribute string. + type ClassProp = false | string | Record | ClassProp[] - // The `memo` function stores a view along with properties for it. - function memo(view: View, props: PropList): Partial> + // The `style` property represents inline CSS. + type StyleProp + = { [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null } + // For some reason we need this to prevent `style` from being a string. + & { [index: number]: never } - // The `text` function creates a virtual DOM node representing plain text. - function text(value: number | string, node?: Node): VDOM + // Event handlers are implemented with actions. + type EventActions = { [K in keyof EventsMap]?: Action } + type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } // --------------------------------------------------------------------------- From 3a21823cb59d8b379af3cb87f158d70697020a18 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 21 Sep 2020 03:55:53 -0400 Subject: [PATCH 27/46] Unify `Payload` with `EffectData` and add flexibility. --- types/index.d.ts | 92 +++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 6c6e8a797..6c3099ade 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,98 +3,95 @@ declare module "hyperapp" { // The `app` function initiates a Hyperapp application. `app` along with // effects are the only places where side effects are allowed. - function app(props: App): Dispatch + function app(props: App): Dispatch // The `h` function builds a virtual DOM node. - function h( + function h( type: string, - props: PropList, - children?: VNode | readonly VNode[], - ): VDOM + props: PropList, + children?: VNode | readonly VNode[], + ): VDOM // The `memo` function stores a view along with properties for it. - function memo(view: View, props: PropList): Partial> + function memo(view: View, props: PropList): Partial> // The `text` function creates a virtual DOM node representing plain text. - function text(value: number | string, node?: Node): VDOM + function text(value: number | string, node?: Node): VDOM // --------------------------------------------------------------------------- // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. - type App + type App = Readonly<{ - init: Transition | Action - view: View + init: Transition | Action + view: View node: Node - subscriptions?: Subscription - middleware?: Middleware + subscriptions?: Subscription + middleware?: Middleware }> // A view builds a virtual DOM node representation of the application state. - type View = (state: State) => VDOM + type View = (state: State) => VDOM // A subscription is a set of recurring effects. - type Subscription = (state: State) => Subscriber[] + type Subscription = (state: State) => Subscriber[] // A subscriber reacts to subscription updates. - type Subscriber = boolean | undefined | Effect | Unsubscribe + type Subscriber = boolean | undefined | Effect | Unsubscribe // A subscriber ideally provides a function that cancels itself properly. type Unsubscribe = () => void // Middleware allows for custom processing during dispatching. - type Middleware = (dispatch: Dispatch) => Dispatch + type Middleware = (dispatch: Dispatch) => Dispatch // --------------------------------------------------------------------------- // A dispatched action handles an event in the context of the current state. - type Dispatch = (action: Action, props?: Payload

) => void + type Dispatch = (action: Action, props?: Payload) => void // An action transforms existing state and/or wraps another action. - type Action - = ((state: State, props?: Payload

) => Transition | Action) - | ActionDescriptor + type Action + = ((state: State, props?: Payload

) => Transition | Action) + | ActionDescriptor // An action descriptor describes an action and any payload for it. - type ActionDescriptor = [Action, Payload

] + type ActionDescriptor = [Action, Payload

] - // A payload is data external to state that is given to a dispatched action. - type Payload

= P + // A payload is data external to state that is given to an action or effect. + type Payload

= P // A transition is a state transformation with any effects to run. - type Transition = State | StateWithEffects + type Transition = State | StateWithEffects // Application state is accessible in every view, action, and subscription. type State = S // Transformed state can be paired with a list of effects to run. - type StateWithEffects = [State, ...EffectDescriptor[]] + type StateWithEffects = [State, ...EffectDescriptor[]] // An effect descriptor describes how an effect should be invoked. // A function that creates this is called an effect constructor. - type EffectDescriptor = [Effect, EffectData] + type EffectDescriptor = [Effect, Payload] // An effect is where side effects and any additional dispatching occur. // An effect used in a subscription should be able to unsubscribe. - type Effect - = (dispatch: Dispatch, props?: EffectData) => - void | Unsubscribe | Promise - - // An effect is generally given additional data. - type EffectData = D + type Effect + = (dispatch: Dispatch, props?: Payload) => + void | Unsubscribe | Promise // --------------------------------------------------------------------------- // A virtual DOM node represents an actual DOM element. - type VDOM = { + type VDOM = { readonly type: string - readonly props: PropList - readonly children: VNode[] + readonly props: PropList + readonly children: VNode[] node: MaybeNode - readonly tag?: Tag + readonly tag?: Tag readonly key: Key - memo?: PropList + memo?: PropList } // A key can uniquely associate a virtual DOM node with a certain DOM element. @@ -104,19 +101,19 @@ declare module "hyperapp" { type MaybeNode = null | undefined | Node // A virtual node is a convenience layer over a virtual DOM node. - type VNode = false | null | undefined | VDOM + type VNode = false | null | undefined | VDOM // A virtual DOM node's tag has metadata relevant to it. Virtual DOM nodes are // tagged by their type to assist rendering. - type Tag = VDOMNodeType | View + type Tag = VDOMNodeType | View // These are based on actual DOM node types: // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType const enum VDOMNodeType { SSR = 1, Text = 3 } // Virtual DOM properties will often correspond to HTML attributes. - type PropList - = Readonly & { + type PropList + = Readonly & { [_: string]: unknown class?: ClassProp key?: Key @@ -132,16 +129,17 @@ declare module "hyperapp" { // For some reason we need this to prevent `style` from being a string. & { [index: number]: never } - // Event handlers are implemented with actions. - type EventActions = { [K in keyof EventsMap]?: Action } + // Event handlers are implemented using actions. + type EventActions = { [K in keyof EventsMap]?: Action } type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } // --------------------------------------------------------------------------- - // Due to current limitations with TypeScript (which should get resolved in - // the future: https://github.com/microsoft/TypeScript/pull/40336), here is - // a collection of modified copies of relevant event maps from TypeScript's - // "lib.dom.d.ts" definition file to assist with defining `EventActions`: + // Due to current limitations with TypeScript (which will get resolved in the + // future: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#key-remapping-mapped-types), + // here is a collection of modified copies of relevant event maps from + // TypeScript's "lib.dom.d.ts" definition file to assist with defining + // `EventActions`: type OnElementEventMap = { "onfullscreenchange": Event From 0f9432addfba7888468cfe055c9b79900f04e5e8 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 21 Sep 2020 04:02:21 -0400 Subject: [PATCH 28/46] Update type tests. --- types/test/actions.test.ts | 62 +++++++++++++--------------- types/test/app.test.ts | 10 ++--- types/test/effects.test.ts | 26 ++++++------ types/test/h.test.ts | 84 +++++++++++++++++++------------------- types/test/text.test.ts | 16 ++++---- types/tslint.json | 3 +- 6 files changed, 100 insertions(+), 101 deletions(-) diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index f84769096..e52e775e1 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -1,40 +1,36 @@ import { - Dispatch, EffectData, EffectDescriptor, State, Transition, + Dispatch, Effect, EffectDescriptor, Payload, State, Transition, app, h, text, } from "hyperapp" -type Test = { x: number } +type Test = { x: number, y: number } -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, - view: (state) => h("button", { - onclick: (state) => ({ ...state, x: state.x * 2 }) - }, [text(state.x)]), - node: document.body -}) - -// $ExpectType Dispatch -app({ - init: { x: 2 }, - view: (state) => h("button", { - onclick: (state) => [{ ...state, x: state.x * 2 }] - }, [text(state.x)]), + init: { x: 2, y: 4 }, + view: (state) => h("div", {}, [ + h("button", { + onclick: (state) => ({ ...state, x: state.x * 2 }) + }, [text(state.x)]), + h("button", { + onclick: (state) => [{ ...state, x: state.x * 2 }] + }, [text(state.x)]), + ]), node: document.body }) // ----------------------------------------------------------------------------- -const runJustEcho = (_dispatch: Dispatch, data: EffectData) => { +const runJustEcho = ((_dispatch: Dispatch, data: Payload): void => { console.log(data) -} +}) as Effect -const justEcho =

(x: string): EffectDescriptor => +const justEcho = (x: string): EffectDescriptor => [runJustEcho, x] -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, + init: { x: 2, y: 4 }, view: (state) => h("button", { onclick: (state) => [ { ...state, x: state.x * 2 }, @@ -44,11 +40,11 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, + init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): Transition => [ + onclick: (state: State): Transition => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -56,11 +52,11 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, + init: { x: 2, y: 4 }, view: (state) => h("button", { - onkeypress: (state: State): Transition => [ + onkeypress: (state: State): Transition => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -68,9 +64,9 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, + init: { x: 2, y: 4 }, view: (state) => h("button", { onclick: (state) => [ { ...state, x: state.x * 2 }, @@ -84,15 +80,15 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ - init: { x: 2 }, + init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): Transition => [ + onclick: (state: State): Transition => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], - onkeypress: (state: State): Transition => [ + onkeypress: (state: State): Transition => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], diff --git a/types/test/app.test.ts b/types/test/app.test.ts index 032ac2f9c..c6d47763b 100644 --- a/types/test/app.test.ts +++ b/types/test/app.test.ts @@ -23,14 +23,14 @@ app(undefined) // $ExpectError type Test = { bar?: number, foo: number } -// $ExpectType Dispatch +// $ExpectType Dispatch app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.bar ?? "")]), node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ init: { foo: 2 }, view: (state) => h("div", {}, [ @@ -43,7 +43,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ init: { foo: 2 }, view: (state) => h("div", {}, [ @@ -63,7 +63,7 @@ app({ // ----------------------------------------------------------------------------- -// $ExpectType Dispatch +// $ExpectType Dispatch app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), @@ -71,7 +71,7 @@ app({ middleware: (dispatch) => dispatch }) -// $ExpectType Dispatch +// $ExpectType Dispatch app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts index 54386ce84..bcc83eef5 100644 --- a/types/test/effects.test.ts +++ b/types/test/effects.test.ts @@ -1,15 +1,17 @@ -import { Effect, EffectDescriptor } from "hyperapp" +// TODO: + +import { Dispatch, EffectDescriptor, Payload } from "hyperapp" -// $ExpectType Effect -const runEcho = ((dispatch, data) => { +const runEcho = (dispatch: Dispatch, data?: Payload): void => { + if (!data) return console.log(data) dispatch((state, x) => state + x, data) -}) as Effect +} -const echo = (x: string): EffectDescriptor => +const echo = (x: string): EffectDescriptor => [runEcho, x] -// $ExpectType EffectDescriptor +// $ExpectType EffectDescriptor echo("hi") // ----------------------------------------------------------------------------- @@ -22,18 +24,18 @@ echo("hi") // Credit: https://gist.github.com/eteeselink/81314282c95cd692ea1d const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) -// $ExpectType Effect -const runEchoEventually = (async (dispatch, data) => { +const runEchoEventually = async (dispatch: Dispatch, data?: Payload): Promise => { + if (!data) return await delay(5000) console.log(data) window.requestAnimationFrame(() => dispatch((state, x) => state + x, data)) -}) as Effect +} -const sayEchoEventually = (x: string): EffectDescriptor => +const echoEventually = (x: string): EffectDescriptor => [runEchoEventually, x] -// $ExpectType EffectDescriptor -sayEchoEventually("hi") +// $ExpectType EffectDescriptor +echoEventually("hi") // ----------------------------------------------------------------------------- diff --git a/types/test/h.test.ts b/types/test/h.test.ts index 835bfc49d..ae338e744 100644 --- a/types/test/h.test.ts +++ b/types/test/h.test.ts @@ -27,8 +27,8 @@ h(-123, {}) // $ExpectError h(-Infinity, {}) // $ExpectError h(Infinity, {}) // $ExpectError h(NaN, {}) // $ExpectError -h("", {}) // $ExpectType VDOM -h("hi", {}) // $ExpectType VDOM +h("", {}) // $ExpectType VDOM +h("hi", {}) // $ExpectType VDOM h({}, {}) // $ExpectError h(new Set(), {}) // $ExpectError h([], {}) // $ExpectError @@ -47,7 +47,7 @@ h("p", Infinity) // $ExpectError h("p", NaN) // $ExpectError h("p", "") // $ExpectError h("p", "hi") // $ExpectError -h("p", {}) // $ExpectType VDOM +h("p", {}) // $ExpectType VDOM h("p", new Set()) // $ExpectError h("p", []) // $ExpectError h("p", Symbol()) // $ExpectError @@ -56,7 +56,7 @@ h("p", null) // $ExpectError h("p", undefined) // $ExpectError h("p", {}, true) // $ExpectError -h("p", {}, false) // $ExpectType VDOM +h("p", {}, false) // $ExpectType VDOM h("p", {}, 0) // $ExpectError h("p", {}, 2424) // $ExpectError h("p", {}, -123) // $ExpectError @@ -67,34 +67,34 @@ h("p", {}, "") // $ExpectError h("p", {}, "hi") // $ExpectError h("p", {}, {}) // $ExpectError h("p", {}, new Set()) // $ExpectError -h("p", {}, []) // $ExpectType VDOM +h("p", {}, []) // $ExpectType VDOM h("p", {}, Symbol()) // $ExpectError h("p", {}, () => {}) // $ExpectError -h("p", {}, null) // $ExpectType VDOM -h("p", {}, undefined) // $ExpectType VDOM +h("p", {}, null) // $ExpectType VDOM +h("p", {}, undefined) // $ExpectType VDOM // ----------------------------------------------------------------------------- h("p", { class: true }) // $ExpectError -h("p", { class: false }) // $ExpectType VDOM +h("p", { class: false }) // $ExpectType VDOM h("p", { class: 0 }) // $ExpectError h("p", { class: 2424 }) // $ExpectError h("p", { class: -123 }) // $ExpectError h("p", { class: -Infinity }) // $ExpectError h("p", { class: Infinity }) // $ExpectError h("p", { class: NaN }) // $ExpectError -h("p", { class: "" }) // $ExpectType VDOM -h("p", { class: "hi" }) // $ExpectType VDOM -h("p", { class: {} }) // $ExpectType VDOM +h("p", { class: "" }) // $ExpectType VDOM +h("p", { class: "hi" }) // $ExpectType VDOM +h("p", { class: {} }) // $ExpectType VDOM h("p", { class: new Set() }) // $ExpectError -h("p", { class: [] }) // $ExpectType VDOM +h("p", { class: [] }) // $ExpectType VDOM h("p", { class: Symbol() }) // $ExpectError h("p", { class: () => {} }) // $ExpectError h("p", { class: null }) // $ExpectError -h("p", { class: undefined }) // $ExpectType VDOM +h("p", { class: undefined }) // $ExpectType VDOM -h("p", { class: { a: true } }) // $ExpectType VDOM -h("p", { class: { a: false } }) // $ExpectType VDOM +h("p", { class: { a: true } }) // $ExpectType VDOM +h("p", { class: { a: false } }) // $ExpectType VDOM h("p", { class: { a: 0 } }) // $ExpectError h("p", { class: { a: 2424 } }) // $ExpectError h("p", { class: { a: -123 } }) // $ExpectError @@ -112,28 +112,28 @@ h("p", { class: { a: null } }) // $ExpectError h("p", { class: { a: undefined } }) // $ExpectError h("p", { class: [true] }) // $ExpectError -h("p", { class: [false] }) // $ExpectType VDOM +h("p", { class: [false] }) // $ExpectType VDOM h("p", { class: [0] }) // $ExpectError h("p", { class: [2424] }) // $ExpectError h("p", { class: [-123] }) // $ExpectError h("p", { class: [-Infinity] }) // $ExpectError h("p", { class: [Infinity] }) // $ExpectError h("p", { class: [NaN] }) // $ExpectError -h("p", { class: [""] }) // $ExpectType VDOM -h("p", { class: ["hi"] }) // $ExpectType VDOM -h("p", { class: [{}] }) // $ExpectType VDOM +h("p", { class: [""] }) // $ExpectType VDOM +h("p", { class: ["hi"] }) // $ExpectType VDOM +h("p", { class: [{}] }) // $ExpectType VDOM h("p", { class: [new Set()] }) // $ExpectError -h("p", { class: [[]] }) // $ExpectType VDOM +h("p", { class: [[]] }) // $ExpectType VDOM h("p", { class: [Symbol()] }) // $ExpectError h("p", { class: [() => {}] }) // $ExpectError h("p", { class: [null] }) // $ExpectError h("p", { class: [undefined] }) // $ExpectError -h("p", { class: [{}] }) // $ExpectType VDOM -h("p", { class: [{ a: true }] }) // $ExpectType VDOM -h("p", { class: [{ a: false }] }) // $ExpectType VDOM +h("p", { class: [{}] }) // $ExpectType VDOM +h("p", { class: [{ a: true }] }) // $ExpectType VDOM +h("p", { class: [{ a: false }] }) // $ExpectType VDOM -h("p", { class: [false && "foo"] }) // $ExpectType VDOM +h("p", { class: [false && "foo"] }) // $ExpectType VDOM // ----------------------------------------------------------------------------- @@ -147,13 +147,13 @@ h("p", { style: Infinity }) // $ExpectError h("p", { style: NaN }) // $ExpectError h("p", { style: "" }) // $ExpectError h("p", { style: "hi" }) // $ExpectError -h("p", { style: {} }) // $ExpectType VDOM +h("p", { style: {} }) // $ExpectType VDOM h("p", { style: new Set() }) // $ExpectError h("p", { style: [] }) // $ExpectError h("p", { style: Symbol() }) // $ExpectError h("p", { style: () => {} }) // $ExpectError h("p", { style: null }) // $ExpectError -h("p", { style: undefined }) // $ExpectType VDOM +h("p", { style: undefined }) // $ExpectType VDOM h("p", { style: { color: true } }) // $ExpectError h("p", { style: { color: false } }) // $ExpectError @@ -163,15 +163,15 @@ h("p", { style: { color: -123 } }) // $ExpectError h("p", { style: { color: -Infinity } }) // $ExpectError h("p", { style: { color: Infinity } }) // $ExpectError h("p", { style: { color: NaN } }) // $ExpectError -h("p", { style: { color: "" } }) // $ExpectType VDOM -h("p", { style: { color: "red" } }) // $ExpectType VDOM +h("p", { style: { color: "" } }) // $ExpectType VDOM +h("p", { style: { color: "red" } }) // $ExpectType VDOM h("p", { style: { color: {} } }) // $ExpectError h("p", { style: { color: new Set() } }) // $ExpectError h("p", { style: { color: [] } }) // $ExpectError h("p", { style: { color: Symbol() } }) // $ExpectError h("p", { style: { color: () => {} } }) // $ExpectError -h("p", { style: { color: null } }) // $ExpectType VDOM -h("p", { style: { color: undefined } }) // $ExpectType VDOM +h("p", { style: { color: null } }) // $ExpectType VDOM +h("p", { style: { color: undefined } }) // $ExpectType VDOM h("p", { style: { clor: null } }) // $ExpectError h("p", { style: [true] }) // $ExpectError @@ -194,32 +194,32 @@ h("p", { style: [undefined] }) // $ExpectError // ----------------------------------------------------------------------------- -// $ExpectType VDOM +// $ExpectType VDOM h("a", { id: "unique" }, [h("br", {})]) -// $ExpectType VDOM +// $ExpectType VDOM h("a", { onclick: (state) => state }, [h("br", {})]) // $ExpectError h("a", { onclick: (state) => ({ ...state }) }, [h("br", {})]) -// $ExpectType VDOM +// $ExpectType VDOM h("a", { onclick: (state) => [state] }, [h("br", {})]) // $ExpectError h("a", { onclick: (state) => [{ ...state }] }, [h("br", {})]) -// $ExpectType VDOM +// $ExpectType VDOM h("a", { onclick: ((state: number) => state * 2) }, [h("br", {})]) -// $ExpectType VDOM +// $ExpectType VDOM h("a", { onclick: (state) => state * 2 }, [h("br", {})]) // ----------------------------------------------------------------------------- type Test = { bar?: number, foo: number } -// $ExpectType VDOM +// $ExpectType VDOM h("button", { onclick: (state) => ({ ...state, bar: state.foo * 2 }) }, [text("clicky")]) @@ -227,7 +227,7 @@ h("button", { // ----------------------------------------------------------------------------- h("p", {}, [true]) // $ExpectError -h("p", {}, [false]) // $ExpectType VDOM +h("p", {}, [false]) // $ExpectType VDOM h("p", {}, [0]) // $ExpectError h("p", {}, [2424]) // $ExpectError h("p", {}, [-123]) // $ExpectError @@ -241,9 +241,9 @@ h("p", {}, [new Set()]) // $ExpectError h("p", {}, [[]]) // $ExpectError h("p", {}, [Symbol()]) // $ExpectError h("p", {}, [() => {}]) // $ExpectError -h("p", {}, [null]) // $ExpectType VDOM -h("p", {}, [undefined]) // $ExpectType VDOM +h("p", {}, [null]) // $ExpectType VDOM +h("p", {}, [undefined]) // $ExpectType VDOM -h("p", {}, h("br", {})) // $ExpectType VDOM -h("p", {}, [h("br", {})]) // $ExpectType VDOM -h("p", {}, [text("hello")]) // $ExpectType VDOM +h("p", {}, h("br", {})) // $ExpectType VDOM +h("p", {}, [h("br", {})]) // $ExpectType VDOM +h("p", {}, [text("hello")]) // $ExpectType VDOM diff --git a/types/test/text.test.ts b/types/test/text.test.ts index e345f192e..ad1401169 100644 --- a/types/test/text.test.ts +++ b/types/test/text.test.ts @@ -3,14 +3,14 @@ import { text } from "hyperapp" text() // $ExpectError text(true) // $ExpectError text(false) // $ExpectError -text(0) // $ExpectType VDOM -text(2424) // $ExpectType VDOM -text(-123) // $ExpectType VDOM -text(-Infinity) // $ExpectType VDOM -text(Infinity) // $ExpectType VDOM -text(NaN) // $ExpectType VDOM -text("") // $ExpectType VDOM -text("hi") // $ExpectType VDOM +text(0) // $ExpectType VDOM +text(2424) // $ExpectType VDOM +text(-123) // $ExpectType VDOM +text(-Infinity) // $ExpectType VDOM +text(Infinity) // $ExpectType VDOM +text(NaN) // $ExpectType VDOM +text("") // $ExpectType VDOM +text("hi") // $ExpectType VDOM text({}) // $ExpectError text(new Set()) // $ExpectError text([]) // $ExpectError diff --git a/types/tslint.json b/types/tslint.json index d0b44879d..2821bbe53 100644 --- a/types/tslint.json +++ b/types/tslint.json @@ -10,6 +10,7 @@ "no-const-enum": false, "no-single-declare-module": false, - "no-unnecessary-generics": false + "no-unnecessary-generics": false, + "void-return": false } } From 2c5af2bee7c7c3a44c71f72dc761deb4e47b8a81 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Thu, 24 Sep 2020 11:22:15 -0400 Subject: [PATCH 29/46] Update some types. - Drop `Transition` - Refactor `Action` while adding `ActionTransform` - Add `Transform` - Rename `StateWithEffects` to `EffectfulState` - Adjust tests accordingly --- package.json | 2 +- types/index.d.ts | 23 +++++++++++------------ types/test/actions.test.ts | 14 +++++++------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 34428a172..33469fa2e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "c8": "*", - "dtslint": "^4.0.0", + "dtslint": "^4.0.2", "terser": "*", "twist": "*", "typescript": "^4.0.2" diff --git a/types/index.d.ts b/types/index.d.ts index 6c3099ade..05ba3e281 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -24,7 +24,8 @@ declare module "hyperapp" { // It must also be mounted over an available DOM element. type App = Readonly<{ - init: Transition | Action + // init: Transition | Action + init: State | EffectfulState | Action view: View node: Node subscriptions?: Subscription @@ -49,27 +50,25 @@ declare module "hyperapp" { // --------------------------------------------------------------------------- // A dispatched action handles an event in the context of the current state. - type Dispatch = (action: Action, props?: Payload) => void + type Dispatch = (action: Action, props?: Payload) => void // An action transforms existing state and/or wraps another action. - type Action - = ((state: State, props?: Payload

) => Transition | Action) - | ActionDescriptor - - // An action descriptor describes an action and any payload for it. + type Action = ActionTransform | ActionDescriptor + type ActionTransform = (state: State, props?: Payload

) => + State | EffectfulState | Action type ActionDescriptor = [Action, Payload

] + // A transform carries out the transition from one state to another. + type Transform = (state: State, props?: Payload

) => State | EffectfulState + // A payload is data external to state that is given to an action or effect. type Payload

= P - // A transition is a state transformation with any effects to run. - type Transition = State | StateWithEffects - // Application state is accessible in every view, action, and subscription. type State = S // Transformed state can be paired with a list of effects to run. - type StateWithEffects = [State, ...EffectDescriptor[]] + type EffectfulState = [State, ...EffectDescriptor[]] // An effect descriptor describes how an effect should be invoked. // A function that creates this is called an effect constructor. @@ -79,7 +78,7 @@ declare module "hyperapp" { // An effect used in a subscription should be able to unsubscribe. type Effect = (dispatch: Dispatch, props?: Payload) => - void | Unsubscribe | Promise + void | Unsubscribe | Promise // --------------------------------------------------------------------------- diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index e52e775e1..c413a8154 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -1,5 +1,5 @@ import { - Dispatch, Effect, EffectDescriptor, Payload, State, Transition, + Dispatch, Effect, EffectDescriptor, EffectfulState, Payload, State, app, h, text, } from "hyperapp" @@ -21,9 +21,9 @@ app({ // ----------------------------------------------------------------------------- -const runJustEcho = ((_dispatch: Dispatch, data: Payload): void => { +const runJustEcho = (_dispatch: Dispatch, data?: Payload): void => { console.log(data) -}) as Effect +} const justEcho = (x: string): EffectDescriptor => [runJustEcho, x] @@ -44,7 +44,7 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): Transition => [ + onclick: (state: State): EffectfulState => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -56,7 +56,7 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onkeypress: (state: State): Transition => [ + onkeypress: (state: State): EffectfulState => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -84,11 +84,11 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): Transition => [ + onclick: (state: State): EffectfulState => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], - onkeypress: (state: State): Transition => [ + onkeypress: (state: State): EffectfulState => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], From 3180dda833a956dfbd30d747c9d84c5a63daa050 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 14 Nov 2020 14:35:28 -0500 Subject: [PATCH 30/46] Tweak some minor things. --- package.json | 4 ++-- types/index.d.ts | 1 - types/test/actions.test.ts | 5 +++-- types/test/effects.test.ts | 5 +++-- types/test/h.test.ts | 26 +++++++++++++------------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 33469fa2e..92c45fe22 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,10 @@ }, "devDependencies": { "c8": "*", - "dtslint": "^4.0.2", + "dtslint": "^4.0.5", "terser": "*", "twist": "*", - "typescript": "^4.0.2" + "typescript": "rc" }, "types": "./types" } diff --git a/types/index.d.ts b/types/index.d.ts index 05ba3e281..e3a1e85d0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -24,7 +24,6 @@ declare module "hyperapp" { // It must also be mounted over an available DOM element. type App = Readonly<{ - // init: Transition | Action init: State | EffectfulState | Action view: View node: Node diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index c413a8154..5cb45ad93 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -25,8 +25,9 @@ const runJustEcho = (_dispatch: Dispatch, data?: Payload): void => console.log(data) } -const justEcho = (x: string): EffectDescriptor => - [runJustEcho, x] +const justEcho = (x: string): EffectDescriptor => { + return [runJustEcho, x] +} // $ExpectType Dispatch app({ diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts index bcc83eef5..575b41e0c 100644 --- a/types/test/effects.test.ts +++ b/types/test/effects.test.ts @@ -31,8 +31,9 @@ const runEchoEventually = async (dispatch: Dispatch, data?: Payload dispatch((state, x) => state + x, data)) } -const echoEventually = (x: string): EffectDescriptor => - [runEchoEventually, x] +const echoEventually = (x: string): EffectDescriptor => { + return [runEchoEventually, x] +} // $ExpectType EffectDescriptor echoEventually("hi") diff --git a/types/test/h.test.ts b/types/test/h.test.ts index ae338e744..27747dd65 100644 --- a/types/test/h.test.ts +++ b/types/test/h.test.ts @@ -15,7 +15,7 @@ h({}) // $ExpectError h(new Set()) // $ExpectError h([]) // $ExpectError h(Symbol()) // $ExpectError -h(() => {}) // $ExpectError +h(() => { }) // $ExpectError h(null) // $ExpectError h(undefined) // $ExpectError @@ -33,7 +33,7 @@ h({}, {}) // $ExpectError h(new Set(), {}) // $ExpectError h([], {}) // $ExpectError h(Symbol(), {}) // $ExpectError -h(() => {}, {}) // $ExpectError +h(() => { }, {}) // $ExpectError h(null, {}) // $ExpectError h(undefined, {}) // $ExpectError @@ -51,7 +51,7 @@ h("p", {}) // $ExpectType VDOM h("p", new Set()) // $ExpectError h("p", []) // $ExpectError h("p", Symbol()) // $ExpectError -h("p", () => {}) // $ExpectError +h("p", () => { }) // $ExpectError h("p", null) // $ExpectError h("p", undefined) // $ExpectError @@ -69,7 +69,7 @@ h("p", {}, {}) // $ExpectError h("p", {}, new Set()) // $ExpectError h("p", {}, []) // $ExpectType VDOM h("p", {}, Symbol()) // $ExpectError -h("p", {}, () => {}) // $ExpectError +h("p", {}, () => { }) // $ExpectError h("p", {}, null) // $ExpectType VDOM h("p", {}, undefined) // $ExpectType VDOM @@ -89,7 +89,7 @@ h("p", { class: {} }) // $ExpectType VDOM h("p", { class: new Set() }) // $ExpectError h("p", { class: [] }) // $ExpectType VDOM h("p", { class: Symbol() }) // $ExpectError -h("p", { class: () => {} }) // $ExpectError +h("p", { class: () => { } }) // $ExpectError h("p", { class: null }) // $ExpectError h("p", { class: undefined }) // $ExpectType VDOM @@ -107,9 +107,9 @@ h("p", { class: { a: {} } }) // $ExpectError h("p", { class: { a: new Set() } }) // $ExpectError h("p", { class: { a: [] } }) // $ExpectError h("p", { class: { a: Symbol() } }) // $ExpectError -h("p", { class: { a: () => {} } }) // $ExpectError +h("p", { class: { a: () => { } } }) // $ExpectError h("p", { class: { a: null } }) // $ExpectError -h("p", { class: { a: undefined } }) // $ExpectError +h("p", { class: { a: undefined } }) // $ExpectType VDOM h("p", { class: [true] }) // $ExpectError h("p", { class: [false] }) // $ExpectType VDOM @@ -125,9 +125,9 @@ h("p", { class: [{}] }) // $ExpectType VDOM h("p", { class: [new Set()] }) // $ExpectError h("p", { class: [[]] }) // $ExpectType VDOM h("p", { class: [Symbol()] }) // $ExpectError -h("p", { class: [() => {}] }) // $ExpectError +h("p", { class: [() => { }] }) // $ExpectError h("p", { class: [null] }) // $ExpectError -h("p", { class: [undefined] }) // $ExpectError +h("p", { class: [undefined] }) // $ExpectType VDOM h("p", { class: [{}] }) // $ExpectType VDOM h("p", { class: [{ a: true }] }) // $ExpectType VDOM @@ -151,7 +151,7 @@ h("p", { style: {} }) // $ExpectType VDOM h("p", { style: new Set() }) // $ExpectError h("p", { style: [] }) // $ExpectError h("p", { style: Symbol() }) // $ExpectError -h("p", { style: () => {} }) // $ExpectError +h("p", { style: () => { } }) // $ExpectError h("p", { style: null }) // $ExpectError h("p", { style: undefined }) // $ExpectType VDOM @@ -169,7 +169,7 @@ h("p", { style: { color: {} } }) // $ExpectError h("p", { style: { color: new Set() } }) // $ExpectError h("p", { style: { color: [] } }) // $ExpectError h("p", { style: { color: Symbol() } }) // $ExpectError -h("p", { style: { color: () => {} } }) // $ExpectError +h("p", { style: { color: () => { } } }) // $ExpectError h("p", { style: { color: null } }) // $ExpectType VDOM h("p", { style: { color: undefined } }) // $ExpectType VDOM h("p", { style: { clor: null } }) // $ExpectError @@ -188,7 +188,7 @@ h("p", { style: [{}] }) // $ExpectError h("p", { style: [new Set()] }) // $ExpectError h("p", { style: [[]] }) // $ExpectError h("p", { style: [Symbol()] }) // $ExpectError -h("p", { style: [() => {}] }) // $ExpectError +h("p", { style: [() => { }] }) // $ExpectError h("p", { style: [null] }) // $ExpectError h("p", { style: [undefined] }) // $ExpectError @@ -240,7 +240,7 @@ h("p", {}, [{}]) // $ExpectError h("p", {}, [new Set()]) // $ExpectError h("p", {}, [[]]) // $ExpectError h("p", {}, [Symbol()]) // $ExpectError -h("p", {}, [() => {}]) // $ExpectError +h("p", {}, [() => { }]) // $ExpectError h("p", {}, [null]) // $ExpectType VDOM h("p", {}, [undefined]) // $ExpectType VDOM From 2486626d9cfdcc4eb02a8bfe3fb8602efad93a87 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 14 Nov 2020 14:43:36 -0500 Subject: [PATCH 31/46] Let `ClassProp` have `undefined`. --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index e3a1e85d0..e2812688f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -119,7 +119,7 @@ declare module "hyperapp" { }> // The `class` property represents an HTML class attribute string. - type ClassProp = false | string | Record | ClassProp[] + type ClassProp = false | string | undefined | Record | ClassProp[] // The `style` property represents inline CSS. type StyleProp From 02685cb26cac3aab5697f1c9fe3138ce0059eb3c Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 14 Nov 2020 22:10:19 -0500 Subject: [PATCH 32/46] Tweak some things. --- package.json | 2 +- types/index.d.ts | 2 +- types/test/app.test.ts | 2 +- types/test/memo.test.ts | 2 +- types/test/text.test.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 92c45fe22..6762c316a 100644 --- a/package.json +++ b/package.json @@ -37,5 +37,5 @@ "twist": "*", "typescript": "rc" }, - "types": "./types" + "types": "./types/index.d.ts" } diff --git a/types/index.d.ts b/types/index.d.ts index e2812688f..2962ece23 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -13,7 +13,7 @@ declare module "hyperapp" { ): VDOM // The `memo` function stores a view along with properties for it. - function memo(view: View, props: PropList): Partial> + function memo(tag: View, memo: PropList): Partial> // The `text` function creates a virtual DOM node representing plain text. function text(value: number | string, node?: Node): VDOM diff --git a/types/test/app.test.ts b/types/test/app.test.ts index c6d47763b..81259ee34 100644 --- a/types/test/app.test.ts +++ b/types/test/app.test.ts @@ -15,7 +15,7 @@ app({}) // $ExpectError app(new Set()) // $ExpectError app([]) // $ExpectError app(Symbol()) // $ExpectError -app(() => {}) // $ExpectError +app(() => { }) // $ExpectError app(null) // $ExpectError app(undefined) // $ExpectError diff --git a/types/test/memo.test.ts b/types/test/memo.test.ts index 8752e5288..eba8eedf6 100644 --- a/types/test/memo.test.ts +++ b/types/test/memo.test.ts @@ -15,7 +15,7 @@ memo({}) // $ExpectError memo(new Set()) // $ExpectError memo([]) // $ExpectError memo(Symbol()) // $ExpectError -memo(() => {}) // $ExpectError +memo(() => { }) // $ExpectError memo(null) // $ExpectError memo(undefined) // $ExpectError diff --git a/types/test/text.test.ts b/types/test/text.test.ts index ad1401169..9010c4e0b 100644 --- a/types/test/text.test.ts +++ b/types/test/text.test.ts @@ -15,6 +15,6 @@ text({}) // $ExpectError text(new Set()) // $ExpectError text([]) // $ExpectError text(Symbol()) // $ExpectError -text(() => {}) // $ExpectError +text(() => { }) // $ExpectError text(null) // $ExpectError text(undefined) // $ExpectError From 4f74af85b3f3d9b13a7f07e43774a55154bef088 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 15 Nov 2020 01:40:29 -0500 Subject: [PATCH 33/46] Include TypeScript definition when installed as a dependency. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6762c316a..782943132 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "repository": "jorgebucaran/hyperapp", "homepage": "https://hyperapp.dev", "files": [ - "*.js*" + "*.js*", + "./types/index.d.ts" ], "author": "Jorge Bucaran", "funding": "https://github.com/sponsors/jorgebucaran", From 271bf376dbec9bb3f67f616a9a10b551edf681b8 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 15 Nov 2020 04:13:03 -0500 Subject: [PATCH 34/46] Work on types. --- package.json | 6 +++--- types/index.d.ts | 5 +++++ types/test/subscriptions.test.ts | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 types/test/subscriptions.test.ts diff --git a/package.json b/package.json index 782943132..408b9197c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "homepage": "https://hyperapp.dev", "files": [ "*.js*", - "./types/index.d.ts" + "*.d.ts" ], "author": "Jorge Bucaran", "funding": "https://github.com/sponsors/jorgebucaran", @@ -29,7 +29,7 @@ "create": "npm run build && git commit -am $msg && git tag -s $msg -m $msg && git push && git push --tags", "message": "echo ${pkg:+@$npm_package_name/$pkg@}$(node -p \"require('./${pkg:+pkg/$pkg/}package').version\")", "release": "env msg=$(npm run -s message) npm run create && cd ./${pkg:+pkg/$pkg} && npm publish --access public", - "dtslint": "dtslint types" + "dts": "dtslint types" }, "devDependencies": { "c8": "*", @@ -38,5 +38,5 @@ "twist": "*", "typescript": "rc" }, - "types": "./types/index.d.ts" + "types": "types" } diff --git a/types/index.d.ts b/types/index.d.ts index 2962ece23..fb05c2e94 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -133,6 +133,11 @@ declare module "hyperapp" { // --------------------------------------------------------------------------- + // TODO: + // - setting up for TypeScript 4.1... + // type OnHTMLElementEventMap = { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] } + // type OnWindowEventMap = { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] } + // Due to current limitations with TypeScript (which will get resolved in the // future: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#key-remapping-mapped-types), // here is a collection of modified copies of relevant event maps from diff --git a/types/test/subscriptions.test.ts b/types/test/subscriptions.test.ts new file mode 100644 index 000000000..e437cfd7b --- /dev/null +++ b/types/test/subscriptions.test.ts @@ -0,0 +1,21 @@ +// TODO: + +import type { Dispatch, State, Subscriber } from "hyperapp" + +import { app, h, text } from "hyperapp" + +const fooSubscription = (_dispatch: Dispatch): void => { + console.log("foo sub") +} + +type Test = { foo: number } + +// $ExpectType Dispatch +app({ + init: { foo: 2 }, + view: (state) => h("p", {}, [text(state.foo)]), + subscriptions: (_state: State): Subscriber[] => [ + fooSubscription, + ], + node: document.body +}) From 3c2ac5b9dd6b790e58be3cc9072c015a20da84d2 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 15 Nov 2020 04:16:39 -0500 Subject: [PATCH 35/46] Tweak how the .d.ts file is referenced. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 408b9197c..b0117f17c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "homepage": "https://hyperapp.dev", "files": [ "*.js*", - "*.d.ts" + "types/index.d.ts" ], "author": "Jorge Bucaran", "funding": "https://github.com/sponsors/jorgebucaran", @@ -38,5 +38,5 @@ "twist": "*", "typescript": "rc" }, - "types": "types" + "types": "types/index.d.ts" } From 11f79e99c0dfb3fb40b784bd49826d96bbe00947 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 16 Nov 2020 01:51:15 -0500 Subject: [PATCH 36/46] Fix `main` prop in package.json. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0117f17c..968f3967f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "The tiny framework for building hypertext applications", "version": "2.0.8", "type": "module", - "main": "hyperapp.js", + "main": "index.js", "license": "MIT", "repository": "jorgebucaran/hyperapp", "homepage": "https://hyperapp.dev", From 149358fc04d8c43353d6448cc95b911b76dbbc16 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 29 Nov 2020 18:15:04 -0500 Subject: [PATCH 37/46] Tweak linting. --- types/tslint.json | 1 + 1 file changed, 1 insertion(+) diff --git a/types/tslint.json b/types/tslint.json index 2821bbe53..3ba90b2b7 100644 --- a/types/tslint.json +++ b/types/tslint.json @@ -9,6 +9,7 @@ "semicolon": false, "no-const-enum": false, + "no-duplicate-imports": false, "no-single-declare-module": false, "no-unnecessary-generics": false, "void-return": false From 4e9f5a63973a4458aaaeb8d07c03f980f391d68a Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 29 Nov 2020 18:35:27 -0500 Subject: [PATCH 38/46] Fix the type definition for the `app`. --- types/index.d.ts | 4 ++-- types/test/actions.test.ts | 12 ++++++------ types/test/app.test.ts | 10 +++++----- types/test/subscriptions.test.ts | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index fb05c2e94..ebac2260a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,9 +1,9 @@ -// Minimum TypeScript Version: 3.7 +// Minimum TypeScript Version: 3.8 declare module "hyperapp" { // The `app` function initiates a Hyperapp application. `app` along with // effects are the only places where side effects are allowed. - function app(props: App): Dispatch + function app(props: App): void // The `h` function builds a virtual DOM node. function h( diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index 5cb45ad93..209de3770 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -5,7 +5,7 @@ import { type Test = { x: number, y: number } -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("div", {}, [ @@ -29,7 +29,7 @@ const justEcho = (x: string): EffectDescriptor => { return [runJustEcho, x] } -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("button", { @@ -41,7 +41,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("button", { @@ -53,7 +53,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("button", { @@ -65,7 +65,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("button", { @@ -81,7 +81,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { x: 2, y: 4 }, view: (state) => h("button", { diff --git a/types/test/app.test.ts b/types/test/app.test.ts index 81259ee34..21c55ee3b 100644 --- a/types/test/app.test.ts +++ b/types/test/app.test.ts @@ -23,14 +23,14 @@ app(undefined) // $ExpectError type Test = { bar?: number, foo: number } -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.bar ?? "")]), node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("div", {}, [ @@ -43,7 +43,7 @@ app({ node: document.body }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("div", {}, [ @@ -63,7 +63,7 @@ app({ // ----------------------------------------------------------------------------- -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), @@ -71,7 +71,7 @@ app({ middleware: (dispatch) => dispatch }) -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), diff --git a/types/test/subscriptions.test.ts b/types/test/subscriptions.test.ts index e437cfd7b..6aeffe72a 100644 --- a/types/test/subscriptions.test.ts +++ b/types/test/subscriptions.test.ts @@ -10,7 +10,7 @@ const fooSubscription = (_dispatch: Dispatch): void => { type Test = { foo: number } -// $ExpectType Dispatch +// $ExpectType void app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), From 2e7c905fc5a5241fd18dbcbfeee5b1de273b7716 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Mon, 7 Dec 2020 02:37:07 -0500 Subject: [PATCH 39/46] Take advantage of template literal types. --- package.json | 4 +- types/index.d.ts | 245 +---------------------------------------------- 2 files changed, 7 insertions(+), 242 deletions(-) diff --git a/package.json b/package.json index 968f3967f..4d54de5c2 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ }, "devDependencies": { "c8": "*", - "dtslint": "^4.0.5", + "dtslint": "*", "terser": "*", "twist": "*", - "typescript": "rc" + "typescript": "*" }, "types": "types/index.d.ts" } diff --git a/types/index.d.ts b/types/index.d.ts index ebac2260a..02d514bca 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,4 @@ -// Minimum TypeScript Version: 3.8 +// Minimum TypeScript Version: 4.2 declare module "hyperapp" { // The `app` function initiates a Hyperapp application. `app` along with @@ -129,243 +129,8 @@ declare module "hyperapp" { // Event handlers are implemented using actions. type EventActions = { [K in keyof EventsMap]?: Action } - type EventsMap = OnHTMLElementEventMap & OnWindowEventMap & { onsearch: Event } - - // --------------------------------------------------------------------------- - - // TODO: - // - setting up for TypeScript 4.1... - // type OnHTMLElementEventMap = { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] } - // type OnWindowEventMap = { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] } - - // Due to current limitations with TypeScript (which will get resolved in the - // future: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#key-remapping-mapped-types), - // here is a collection of modified copies of relevant event maps from - // TypeScript's "lib.dom.d.ts" definition file to assist with defining - // `EventActions`: - - type OnElementEventMap = { - "onfullscreenchange": Event - "onfullscreenerror": Event - } - - type OnGlobalEventHandlersEventMap = { - "onabort": UIEvent - "onanimationcancel": AnimationEvent - "onanimationend": AnimationEvent - "onanimationiteration": AnimationEvent - "onanimationstart": AnimationEvent - "onauxclick": MouseEvent - "onblur": FocusEvent - "oncancel": Event - "oncanplay": Event - "oncanplaythrough": Event - "onchange": Event - "onclick": MouseEvent - "onclose": Event - "oncontextmenu": MouseEvent - "oncuechange": Event - "ondblclick": MouseEvent - "ondrag": DragEvent - "ondragend": DragEvent - "ondragenter": DragEvent - "ondragexit": Event - "ondragleave": DragEvent - "ondragover": DragEvent - "ondragstart": DragEvent - "ondrop": DragEvent - "ondurationchange": Event - "onemptied": Event - "onended": Event - "onerror": ErrorEvent - "onfocus": FocusEvent - "onfocusin": FocusEvent - "onfocusout": FocusEvent - "ongotpointercapture": PointerEvent - "oninput": Event - "oninvalid": Event - "onkeydown": KeyboardEvent - "onkeypress": KeyboardEvent - "onkeyup": KeyboardEvent - "onload": Event - "onloadeddata": Event - "onloadedmetadata": Event - "onloadstart": Event - "onlostpointercapture": PointerEvent - "onmousedown": MouseEvent - "onmouseenter": MouseEvent - "onmouseleave": MouseEvent - "onmousemove": MouseEvent - "onmouseout": MouseEvent - "onmouseover": MouseEvent - "onmouseup": MouseEvent - "onpause": Event - "onplay": Event - "onplaying": Event - "onpointercancel": PointerEvent - "onpointerdown": PointerEvent - "onpointerenter": PointerEvent - "onpointerleave": PointerEvent - "onpointermove": PointerEvent - "onpointerout": PointerEvent - "onpointerover": PointerEvent - "onpointerup": PointerEvent - "onprogress": ProgressEvent - "onratechange": Event - "onreset": Event - "onresize": UIEvent - "onscroll": Event - "onsecuritypolicyviolation": SecurityPolicyViolationEvent - "onseeked": Event - "onseeking": Event - "onselect": Event - "onselectionchange": Event - "onselectstart": Event - "onstalled": Event - "onsubmit": Event - "onsuspend": Event - "ontimeupdate": Event - "ontoggle": Event - "ontouchcancel": TouchEvent - "ontouchend": TouchEvent - "ontouchmove": TouchEvent - "ontouchstart": TouchEvent - "ontransitioncancel": TransitionEvent - "ontransitionend": TransitionEvent - "ontransitionrun": TransitionEvent - "ontransitionstart": TransitionEvent - "onvolumechange": Event - "onwaiting": Event - "onwheel": WheelEvent - } - - type OnDocumentAndElementEventHandlersEventMap = { - "oncopy": ClipboardEvent - "oncut": ClipboardEvent - "onpaste": ClipboardEvent - } - - type OnHTMLElementEventMap - = OnElementEventMap - & OnGlobalEventHandlersEventMap - & OnDocumentAndElementEventHandlersEventMap - - type OnWindowEventHandlersEventMap = { - "onafterprint": Event - "onbeforeprint": Event - "onbeforeunload": BeforeUnloadEvent - "onhashchange": HashChangeEvent - "onlanguagechange": Event - "onmessage": MessageEvent - "onmessageerror": MessageEvent - "onoffline": Event - "ononline": Event - "onpagehide": PageTransitionEvent - "onpageshow": PageTransitionEvent - "onpopstate": PopStateEvent - "onrejectionhandled": PromiseRejectionEvent - "onstorage": StorageEvent - "onunhandledrejection": PromiseRejectionEvent - "onunload": Event - } - - type OnWindowEventMap = OnGlobalEventHandlersEventMap & OnWindowEventHandlersEventMap & { - "onabort": UIEvent - "onafterprint": Event - "onbeforeprint": Event - "onbeforeunload": BeforeUnloadEvent - "onblur": FocusEvent - "oncanplay": Event - "oncanplaythrough": Event - "onchange": Event - "onclick": MouseEvent - "oncompassneedscalibration": Event - "oncontextmenu": MouseEvent - "ondblclick": MouseEvent - "ondevicelight": DeviceLightEvent - "ondevicemotion": DeviceMotionEvent - "ondeviceorientation": DeviceOrientationEvent - "ondeviceorientationabsolute": DeviceOrientationEvent - "ondrag": DragEvent - "ondragend": DragEvent - "ondragenter": DragEvent - "ondragleave": DragEvent - "ondragover": DragEvent - "ondragstart": DragEvent - "ondrop": DragEvent - "ondurationchange": Event - "onemptied": Event - "onended": Event - "onerror": ErrorEvent - "onfocus": FocusEvent - "onhashchange": HashChangeEvent - "oninput": Event - "oninvalid": Event - "onkeydown": KeyboardEvent - "onkeypress": KeyboardEvent - "onkeyup": KeyboardEvent - "onload": Event - "onloadeddata": Event - "onloadedmetadata": Event - "onloadstart": Event - "onmessage": MessageEvent - "onmousedown": MouseEvent - "onmouseenter": MouseEvent - "onmouseleave": MouseEvent - "onmousemove": MouseEvent - "onmouseout": MouseEvent - "onmouseover": MouseEvent - "onmouseup": MouseEvent - "onmousewheel": Event - "onMSGestureChange": Event - "onMSGestureDoubleTap": Event - "onMSGestureEnd": Event - "onMSGestureHold": Event - "onMSGestureStart": Event - "onMSGestureTap": Event - "onMSInertiaStart": Event - "onMSPointerCancel": Event - "onMSPointerDown": Event - "onMSPointerEnter": Event - "onMSPointerLeave": Event - "onMSPointerMove": Event - "onMSPointerOut": Event - "onMSPointerOver": Event - "onMSPointerUp": Event - "onoffline": Event - "ononline": Event - "onorientationchange": Event - "onpagehide": PageTransitionEvent - "onpageshow": PageTransitionEvent - "onpause": Event - "onplay": Event - "onplaying": Event - "onpopstate": PopStateEvent - "onprogress": ProgressEvent - "onratechange": Event - "onreadystatechange": ProgressEvent - "onreset": Event - "onresize": UIEvent - "onscroll": Event - "onseeked": Event - "onseeking": Event - "onselect": Event - "onstalled": Event - "onstorage": StorageEvent - "onsubmit": Event - "onsuspend": Event - "ontimeupdate": Event - "onunload": Event - "onvolumechange": Event - "onvrdisplayactivate": Event - "onvrdisplayblur": Event - "onvrdisplayconnect": Event - "onvrdisplaydeactivate": Event - "onvrdisplaydisconnect": Event - "onvrdisplayfocus": Event - "onvrdisplaypointerrestricted": Event - "onvrdisplaypointerunrestricted": Event - "onvrdisplaypresentchange": Event - "onwaiting": Event - } + type EventsMap + = { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] } + & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] } + & { onsearch: Event } } From 5ec4882da2c6fba58ddeedc078c1f164e011e75f Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 27 Dec 2020 23:16:02 -0500 Subject: [PATCH 40/46] Improve perspective of state. --- types/index.d.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 02d514bca..4f9d1d11d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -22,14 +22,13 @@ declare module "hyperapp" { // A Hyperapp application instance has an initial state and a base view. // It must also be mounted over an available DOM element. - type App - = Readonly<{ - init: State | EffectfulState | Action - view: View - node: Node - subscriptions?: Subscription - middleware?: Middleware - }> + type App = Readonly<{ + init: StateFormat | Action + view: View + node: Node + subscriptions?: Subscription + middleware?: Middleware + }> // A view builds a virtual DOM node representation of the application state. type View = (state: State) => VDOM @@ -53,12 +52,14 @@ declare module "hyperapp" { // An action transforms existing state and/or wraps another action. type Action = ActionTransform | ActionDescriptor - type ActionTransform = (state: State, props?: Payload

) => - State | EffectfulState | Action + type ActionTransform = (state: State, props?: Payload

) => StateFormat | Action type ActionDescriptor = [Action, Payload

] // A transform carries out the transition from one state to another. - type Transform = (state: State, props?: Payload

) => State | EffectfulState + type Transform = (state: StateFormat, props?: Payload

) => StateFormat + + // State can either be on its own or associated with effects. + type StateFormat = State | StateWithEffects // A payload is data external to state that is given to an action or effect. type Payload

= P @@ -66,8 +67,8 @@ declare module "hyperapp" { // Application state is accessible in every view, action, and subscription. type State = S - // Transformed state can be paired with a list of effects to run. - type EffectfulState = [State, ...EffectDescriptor[]] + // State can be associated with a list of effects to run. + type StateWithEffects = [State, ...EffectDescriptor[]] // An effect descriptor describes how an effect should be invoked. // A function that creates this is called an effect constructor. From bb36e750019873df345419b364f9d75207c9be98 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 27 Dec 2020 23:17:32 -0500 Subject: [PATCH 41/46] Tweak wording and layout. --- types/index.d.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 4f9d1d11d..af0142194 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,21 +1,21 @@ // Minimum TypeScript Version: 4.2 declare module "hyperapp" { - // The `app` function initiates a Hyperapp application. `app` along with - // effects are the only places where side effects are allowed. + // `app()` initiates a Hyperapp application. `app()` along with effects are + // only places where side effects are allowed. function app(props: App): void - // The `h` function builds a virtual DOM node. + // `h()` builds a virtual DOM node. function h( type: string, props: PropList, children?: VNode | readonly VNode[], ): VDOM - // The `memo` function stores a view along with properties for it. + // `memo()` stores a view along with properties for it. function memo(tag: View, memo: PropList): Partial> - // The `text` function creates a virtual DOM node representing plain text. + // `text()` creates a virtual DOM node representing plain text. function text(value: number | string, node?: Node): VDOM // --------------------------------------------------------------------------- @@ -76,9 +76,8 @@ declare module "hyperapp" { // An effect is where side effects and any additional dispatching occur. // An effect used in a subscription should be able to unsubscribe. - type Effect - = (dispatch: Dispatch, props?: Payload) => - void | Unsubscribe | Promise + type Effect = (dispatch: Dispatch, props?: Payload) => + void | Unsubscribe | Promise // --------------------------------------------------------------------------- @@ -111,13 +110,12 @@ declare module "hyperapp" { const enum VDOMNodeType { SSR = 1, Text = 3 } // Virtual DOM properties will often correspond to HTML attributes. - type PropList - = Readonly & { - [_: string]: unknown - class?: ClassProp - key?: Key - style?: StyleProp - }> + type PropList = Readonly & { + [_: string]: unknown + class?: ClassProp + key?: Key + style?: StyleProp + }> // The `class` property represents an HTML class attribute string. type ClassProp = false | string | undefined | Record | ClassProp[] From 874777d5cdd34575a55eaee1fe60fe217c70b161 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sun, 27 Dec 2020 23:27:06 -0500 Subject: [PATCH 42/46] Update some type tests. --- types/test/actions.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index 209de3770..ae04aca06 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -1,5 +1,5 @@ import { - Dispatch, Effect, EffectDescriptor, EffectfulState, Payload, State, + Dispatch, EffectDescriptor, StateWithEffects, Payload, State, app, h, text, } from "hyperapp" @@ -45,7 +45,7 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): EffectfulState => [ + onclick: (state: State): StateWithEffects => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -57,7 +57,7 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onkeypress: (state: State): EffectfulState => [ + onkeypress: (state: State): StateWithEffects => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], @@ -85,11 +85,11 @@ app({ app({ init: { x: 2, y: 4 }, view: (state) => h("button", { - onclick: (state: State): EffectfulState => [ + onclick: (state: State): StateWithEffects => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], - onkeypress: (state: State): EffectfulState => [ + onkeypress: (state: State): StateWithEffects => [ { ...state, x: state.x * 2 }, justEcho("hi"), ], From be5ecfbee69e120d5a74c4d58f29a63b8906eea0 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Fri, 22 Jan 2021 11:34:25 -0500 Subject: [PATCH 43/46] Add some notes and to the type definitions. --- types/index.d.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index af0142194..9a0a9b446 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,5 @@ // Minimum TypeScript Version: 4.2 +// `dtslint` needs 4.2 even though these definitions should work with 4.1. declare module "hyperapp" { // `app()` initiates a Hyperapp application. `app()` along with effects are @@ -61,9 +62,6 @@ declare module "hyperapp" { // State can either be on its own or associated with effects. type StateFormat = State | StateWithEffects - // A payload is data external to state that is given to an action or effect. - type Payload

= P - // Application state is accessible in every view, action, and subscription. type State = S @@ -79,6 +77,9 @@ declare module "hyperapp" { type Effect = (dispatch: Dispatch, props?: Payload) => void | Unsubscribe | Promise + // A payload is data external to state that is given to an action or effect. + type Payload

= P + // --------------------------------------------------------------------------- // A virtual DOM node represents an actual DOM element. @@ -121,10 +122,18 @@ declare module "hyperapp" { type ClassProp = false | string | undefined | Record | ClassProp[] // The `style` property represents inline CSS. + // + // NOTE: This relies on what TypeScript itself recognizes as valid CSS + // properties. Custom properties are not covered as well as any newer + // properties that are not yet recognized by TypeScript. Apparently, + // the only way to accommodate them is to relax the adherence to + // TypeScript's CSS property definitions. The trade-off doesn't + // seem worth it given the rarity of such properties. However, + // if you need them the workaround is to use type casting. type StyleProp = { [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null } - // For some reason we need this to prevent `style` from being a string. - & { [index: number]: never } + // Since strings are indexable we can avoid them by preventing indexing. + & { [_: number]: never } // Event handlers are implemented using actions. type EventActions = { [K in keyof EventsMap]?: Action } From b505373d63520f6cb58c45aaf1495bd986701b60 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Fri, 22 Jan 2021 11:44:23 -0500 Subject: [PATCH 44/46] Fix a type bug and add some type tests. --- types/index.d.ts | 4 ++++ types/test/h.test.ts | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9a0a9b446..b09729174 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -116,6 +116,10 @@ declare module "hyperapp" { class?: ClassProp key?: Key style?: StyleProp + // This is used to ensure that values that match `VDOM` are not mistaken for + // matching `PropList`. However, if you really need `node` for some reason + // then the workaround is to use type casting. + node?: never }> // The `class` property represents an HTML class attribute string. diff --git a/types/test/h.test.ts b/types/test/h.test.ts index 27747dd65..40054025a 100644 --- a/types/test/h.test.ts +++ b/types/test/h.test.ts @@ -129,10 +129,8 @@ h("p", { class: [() => { }] }) // $ExpectError h("p", { class: [null] }) // $ExpectError h("p", { class: [undefined] }) // $ExpectType VDOM -h("p", { class: [{}] }) // $ExpectType VDOM -h("p", { class: [{ a: true }] }) // $ExpectType VDOM -h("p", { class: [{ a: false }] }) // $ExpectType VDOM - +h("p", { class: [{ a: true }] }) // $ExpectType VDOM +h("p", { class: [{ a: false }] }) // $ExpectType VDOM h("p", { class: [false && "foo"] }) // $ExpectType VDOM // ----------------------------------------------------------------------------- @@ -173,6 +171,11 @@ h("p", { style: { color: () => { } } }) // $ExpectError h("p", { style: { color: null } }) // $ExpectType VDOM h("p", { style: { color: undefined } }) // $ExpectType VDOM h("p", { style: { clor: null } }) // $ExpectError +h("p", { style: { clor: "hi" } }) // $ExpectError + +// We need to use type casting if we want to use custom properties. +h("p", { style: { "--clor": null } as any}) // $ExpectType VDOM +h("p", { style: { "--clor": "hi" } as any}) // $ExpectType VDOM h("p", { style: [true] }) // $ExpectError h("p", { style: [false] }) // $ExpectError @@ -194,6 +197,12 @@ h("p", { style: [undefined] }) // $ExpectError // ----------------------------------------------------------------------------- +// $ExpectType VDOM +h("p", { customThingy: "blahbiddyblah"}, text("hi")) + +// $ExpectType VDOM +h("p", { "data-thingy": "blahbiddyblah"}, text("hi")) + // $ExpectType VDOM h("a", { id: "unique" }, [h("br", {})]) @@ -247,3 +256,7 @@ h("p", {}, [undefined]) // $ExpectType VDOM h("p", {}, h("br", {})) // $ExpectType VDOM h("p", {}, [h("br", {})]) // $ExpectType VDOM h("p", {}, [text("hello")]) // $ExpectType VDOM + +// ----------------------------------------------------------------------------- + +h("p", text("hi")) // $ExpectError From e11e1ce1f0aa44d75cba41dab03c5cc195fa6db5 Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 23 Jan 2021 04:24:14 -0500 Subject: [PATCH 45/46] Introduce a type-level only guard prop to check for VDOM objects. --- types/index.d.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index b09729174..9b98ba35c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -91,6 +91,10 @@ declare module "hyperapp" { readonly tag?: Tag readonly key: Key memo?: PropList + + // `_VDOM` is a guard property which gives us a way to tell `VDOM` objects + // apart from `PropList` object. + _VDOM: true } // A key can uniquely associate a virtual DOM node with a certain DOM element. @@ -116,10 +120,10 @@ declare module "hyperapp" { class?: ClassProp key?: Key style?: StyleProp - // This is used to ensure that values that match `VDOM` are not mistaken for - // matching `PropList`. However, if you really need `node` for some reason - // then the workaround is to use type casting. - node?: never + + // By disallowing `_VDOM` we ensure that values matching `VDOM` are not + // mistaken for also matching `PropList`. + _VDOM?: never }> // The `class` property represents an HTML class attribute string. From 0b7049f35ffadf32cf71a362c560a0719649bc0d Mon Sep 17 00:00:00 2001 From: Ron Martinez Date: Sat, 23 Jan 2021 15:38:07 -0500 Subject: [PATCH 46/46] Overhaul type terminology. --- types/index.d.ts | 35 ++++++++++++++++---------------- types/test/actions.test.ts | 11 ++++------ types/test/app.test.ts | 22 ++++++++++++++++++++ types/test/effects.test.ts | 15 +++++--------- types/test/subscriptions.test.ts | 4 ++-- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9b98ba35c..da6b746d8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,8 +2,8 @@ // `dtslint` needs 4.2 even though these definitions should work with 4.1. declare module "hyperapp" { - // `app()` initiates a Hyperapp application. `app()` along with effects are - // only places where side effects are allowed. + // `app()` initiates a Hyperapp application. `app()` along with runners and + // subscribers are the only places where side effects are allowed. function app(props: App): void // `h()` builds a virtual DOM node. @@ -27,20 +27,23 @@ declare module "hyperapp" { init: StateFormat | Action view: View node: Node - subscriptions?: Subscription + subscriptions?: Subscriptions middleware?: Middleware }> // A view builds a virtual DOM node representation of the application state. type View = (state: State) => VDOM - // A subscription is a set of recurring effects. - type Subscription = (state: State) => Subscriber[] + // The subscriptions function manages a set of subscriptions. + type Subscriptions = (state: State) => Subscription[] + + // A subscription represents subscriber activity. + type Subscription = boolean | undefined | Subscriber | Unsubscribe // A subscriber reacts to subscription updates. - type Subscriber = boolean | undefined | Effect | Unsubscribe + type Subscriber = (dispatch: Dispatch, props?: Payload) => void | Unsubscribe - // A subscriber ideally provides a function that cancels itself properly. + // An unsubscribe function cleans up a canceled subscription. type Unsubscribe = () => void // Middleware allows for custom processing during dispatching. @@ -66,18 +69,16 @@ declare module "hyperapp" { type State = S // State can be associated with a list of effects to run. - type StateWithEffects = [State, ...EffectDescriptor[]] + type StateWithEffects = [State, ...(Effect | RunnerDescriptor)[]] - // An effect descriptor describes how an effect should be invoked. - // A function that creates this is called an effect constructor. - type EffectDescriptor = [Effect, Payload] + // An effect is an abstraction over an impure process. + type Effect = (..._: any[]) => RunnerDescriptor - // An effect is where side effects and any additional dispatching occur. - // An effect used in a subscription should be able to unsubscribe. - type Effect = (dispatch: Dispatch, props?: Payload) => - void | Unsubscribe | Promise + // A runner is where side effects and any additional dispatching may occur. + type RunnerDescriptor = [Runner, Payload] + type Runner = (dispatch: Dispatch, props?: Payload) => void | Promise - // A payload is data external to state that is given to an action or effect. + // A payload is data given to an action, effect, or subscription. type Payload

= P // --------------------------------------------------------------------------- @@ -100,7 +101,7 @@ declare module "hyperapp" { // A key can uniquely associate a virtual DOM node with a certain DOM element. type Key = string | null | undefined - // Actual DOM nodes will be manipulated depending on how property patching goes. + // Actual DOM nodes get manipulated depending on how property patching goes. type MaybeNode = null | undefined | Node // A virtual node is a convenience layer over a virtual DOM node. diff --git a/types/test/actions.test.ts b/types/test/actions.test.ts index ae04aca06..ecd61241b 100644 --- a/types/test/actions.test.ts +++ b/types/test/actions.test.ts @@ -1,7 +1,6 @@ -import { - Dispatch, EffectDescriptor, StateWithEffects, Payload, State, - app, h, text, -} from "hyperapp" +import type { Dispatch, Effect, Payload, State, StateWithEffects } from "hyperapp" + +import { app, h, text } from "hyperapp" type Test = { x: number, y: number } @@ -25,9 +24,7 @@ const runJustEcho = (_dispatch: Dispatch, data?: Payload): void => console.log(data) } -const justEcho = (x: string): EffectDescriptor => { - return [runJustEcho, x] -} +const justEcho: Effect = (x) => [runJustEcho, x] // $ExpectType void app({ diff --git a/types/test/app.test.ts b/types/test/app.test.ts index 21c55ee3b..e6770ded0 100644 --- a/types/test/app.test.ts +++ b/types/test/app.test.ts @@ -1,3 +1,5 @@ +import { Dispatch, Effect, Payload } from "hyperapp" + import { app, h, text } from "hyperapp" app() // $ExpectError @@ -23,6 +25,12 @@ app(undefined) // $ExpectError type Test = { bar?: number, foo: number } +const runTestFx = (dispatch: Dispatch): void => { + console.log("test") +} + +const testFx: Effect = () => [runTestFx, undefined] + // $ExpectType void app({ init: { foo: 2 }, @@ -30,6 +38,20 @@ app({ node: document.body }) +// $ExpectType void +app({ + init: [{ foo: 2 }], + view: (state) => h("p", {}, [text(state.bar ?? "")]), + node: document.body +}) + +// $ExpectType void +app({ + init: [{ foo: 2 }, testFx()], + view: (state) => h("p", {}, [text(state.bar ?? "")]), + node: document.body +}) + // $ExpectType void app({ init: { foo: 2 }, diff --git a/types/test/effects.test.ts b/types/test/effects.test.ts index 575b41e0c..42f9a5bb8 100644 --- a/types/test/effects.test.ts +++ b/types/test/effects.test.ts @@ -1,6 +1,4 @@ -// TODO: - -import { Dispatch, EffectDescriptor, Payload } from "hyperapp" +import { Dispatch, Effect, Payload, RunnerDescriptor } from "hyperapp" const runEcho = (dispatch: Dispatch, data?: Payload): void => { if (!data) return @@ -8,10 +6,9 @@ const runEcho = (dispatch: Dispatch, data?: Payload): void => { dispatch((state, x) => state + x, data) } -const echo = (x: string): EffectDescriptor => - [runEcho, x] +const echo: Effect = (x) => [runEcho, x] -// $ExpectType EffectDescriptor +// $ExpectType RunnerDescriptor echo("hi") // ----------------------------------------------------------------------------- @@ -31,11 +28,9 @@ const runEchoEventually = async (dispatch: Dispatch, data?: Payload dispatch((state, x) => state + x, data)) } -const echoEventually = (x: string): EffectDescriptor => { - return [runEchoEventually, x] -} +const echoEventually: Effect = (x) => [runEchoEventually, x] -// $ExpectType EffectDescriptor +// $ExpectType RunnerDescriptor echoEventually("hi") // ----------------------------------------------------------------------------- diff --git a/types/test/subscriptions.test.ts b/types/test/subscriptions.test.ts index 6aeffe72a..1b4931987 100644 --- a/types/test/subscriptions.test.ts +++ b/types/test/subscriptions.test.ts @@ -1,6 +1,6 @@ // TODO: -import type { Dispatch, State, Subscriber } from "hyperapp" +import type { Dispatch, State, Subscription } from "hyperapp" import { app, h, text } from "hyperapp" @@ -14,7 +14,7 @@ type Test = { foo: number } app({ init: { foo: 2 }, view: (state) => h("p", {}, [text(state.foo)]), - subscriptions: (_state: State): Subscriber[] => [ + subscriptions: (_state: State): Subscription[] => [ fooSubscription, ], node: document.body