packages feed

ihp-ssc (empty) → 1.5.0

raw patch · 10 files changed

+568/−0 lines, 10 filesdep +aesondep +attoparsecdep +base

Dependencies added: aeson, attoparsec, base, basic-prelude, blaze-html, bytestring, ihp, ihp-hsx, ihp-log, megaparsec, string-conversions, text, wai, wai-request-params, websockets

Files

+ IHP/ServerSideComponent/Controller/ComponentsController.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE  UndecidableInstances #-}+module IHP.ServerSideComponent.Controller.ComponentsController where++import IHP.ControllerPrelude+import IHP.ServerSideComponent.Types as SSC+import IHP.ServerSideComponent.ControllerFunctions as SSC++import qualified Data.Aeson as Aeson+import qualified IHP.Log as Log+import qualified Control.Exception as Exception+import Data.Typeable (typeOf)++instance (Component component controller, FromJSON controller, Typeable component) => WSApp (ComponentsController component) where+    initialState = ComponentsController++    run = do+        let state :: component = SSC.initialState+        instanceRef <- newIORef (ComponentInstance { state })+        let ?instanceRef = instanceRef++        let componentName = tshow (typeOf state)+        Log.info ("SSC: Component " <> componentName <> " connected")++        -- Handle componentDidMount with exception handling+        mountResult <- Exception.try (componentDidMount state)+        case mountResult of+            Left (e :: SomeException) -> do+                let errorText = tshow e+                Log.error ("SSC: componentDidMount failed for " <> componentName <> ": " <> errorText)+                SSC.sendError (SSCActionError { errorMessage = "Component initialization failed: " <> errorText })+            Right nextState -> do+                Log.debug ("SSC: Component " <> componentName <> " mounted")+                SSC.setState nextState++        forever do+            actionPayload :: LByteString <- receiveData++            let theAction = Aeson.eitherDecode @controller actionPayload++            case theAction of+                Right theAction -> do+                    currentState <- SSC.getState++                    -- Execute action with exception handling+                    actionResult <- Exception.try (SSC.action currentState theAction)+                    case actionResult of+                        Left (e :: SomeException) -> do+                            let errorText = tshow e+                            Log.error ("SSC: Action failed for " <> componentName <> ": " <> errorText)+                            SSC.sendError (SSCActionError { errorMessage = errorText })+                        Right nextState -> do+                            SSC.setState nextState+                Left parseError -> do+                    let errorText = cs parseError+                    Log.error ("SSC: Failed parsing action for " <> componentName <> ": " <> errorText)+                    Log.debug ("SSC: Invalid payload: " <> tshow actionPayload)+                    SSC.sendError (SSCParseError { errorMessage = errorText })
+ IHP/ServerSideComponent/ControllerFunctions.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE TemplateHaskell #-}+{-|+Module: IHP.ServerSideComponent.ControllerFunctions+Copyright: (c) digitally induced GmbH, 2021+-}+module IHP.ServerSideComponent.ControllerFunctions where++import IHP.Prelude+import IHP.ControllerPrelude+import IHP.ServerSideComponent.Types as SSC++import qualified Network.WebSockets as WebSocket+import qualified Text.Blaze.Html.Renderer.Text as Blaze++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.TH as Aeson++import IHP.ServerSideComponent.HtmlParser+import IHP.ServerSideComponent.HtmlDiff++import qualified IHP.Log as Log++$(Aeson.deriveJSON Aeson.defaultOptions { sumEncoding = defaultTaggedObject { tagFieldName = "type" }} ''Attribute)+$(Aeson.deriveJSON Aeson.defaultOptions { sumEncoding = defaultTaggedObject { tagFieldName = "type" }} ''AttributeOperation)+$(Aeson.deriveJSON Aeson.defaultOptions { sumEncoding = defaultTaggedObject { tagFieldName = "type" }} ''Node)+$(Aeson.deriveJSON Aeson.defaultOptions { sumEncoding = defaultTaggedObject { tagFieldName = "type" }} ''NodeOperation)+$(Aeson.deriveJSON Aeson.defaultOptions { sumEncoding = defaultTaggedObject { tagFieldName = "type" }} ''SSCError)++setState :: (?instanceRef :: IORef (ComponentInstance state), ?connection :: WebSocket.Connection, Component state action, ?context :: ControllerContext, ?request :: Request) => state -> IO ()+setState state = do+    oldState <- (.state) <$> readIORef ?instanceRef+    let oldHtml = oldState+            |> SSC.render+            |> Blaze.renderHtml+            |> cs+    let newHtml = state+            |> SSC.render+            |> Blaze.renderHtml+            |> cs++    modifyIORef' ?instanceRef (\componentInstance -> componentInstance { state })++    case diffHtml oldHtml newHtml of+        Left parseError -> do+            let errorText = tshow parseError+            Log.error ("SSC HTML diff failed: " <> errorText)+            sendError (SSCDiffError { errorMessage = errorText })+        Right patches -> sendTextData (Aeson.encode patches)++-- | Send an error message to the client+sendError :: (?connection :: WebSocket.Connection) => SSCError -> IO ()+sendError error = sendTextData (Aeson.encode error)+++getState :: (?instanceRef :: IORef (ComponentInstance state)) => IO state+getState = (.state) <$> readIORef ?instanceRef++deriveSSC = Aeson.deriveJSON Aeson.defaultOptions { allNullaryToStringTag = False, sumEncoding = defaultTaggedObject { tagFieldName = "action", contentsFieldName = "payload" }}
+ IHP/ServerSideComponent/HtmlDiff.hs view
@@ -0,0 +1,131 @@+{-|+Module: IHP.ServerSideComponent.HtmlDiff+Copyright: (c) digitally induced GmbH, 2021+Description: Provides differences and patchsets between two HTML fragments+-}+module IHP.ServerSideComponent.HtmlDiff where++import IHP.Prelude+import IHP.ServerSideComponent.HtmlParser+import qualified Data.Text as Text+import Text.Megaparsec.Error (ParseErrorBundle)+import Data.Void (Void)++data NodeOperation+    = UpdateTextContent { textContent :: !Text, path :: ![Int] }+    | ReplaceNode { oldNode :: !Node, newNode :: !Node, newNodeHtml :: !Text, path :: ![Int] }+    | UpdateNode { attributeOperations :: ![AttributeOperation], path :: ![Int] }+    | UpdateComment { comment :: !Text, path :: ![Int] }+    | UpdatePreEscapedTextNode { textContent :: !Text, path :: ![Int] }+    | DeleteNode { node :: !Node, path :: ![Int] }+    | CreateNode { html :: !Text, path :: ![Int] }+    deriving (Eq, Show)++data AttributeOperation+    = UpdateAttribute { attributeName :: !Text, attributeValue :: !Text }+    | AddAttribute { attributeName :: !Text, attributeValue :: !Text }+    | DeleteAttribute { attributeName :: !Text }+    deriving (Eq, Show)++diffHtml :: Text -> Text -> Either (ParseErrorBundle Text Void) [NodeOperation]+diffHtml a b = do+    nodeA <- parseHtml a+    nodeB <- parseHtml b++    let ?oldHtml = a+    let ?newHtml = b++    pure (diffNodes nodeA nodeB)++type Path = [Int]++diffNodes :: (?oldHtml :: text, ?newHtml :: Text) => Node -> Node -> [NodeOperation]+diffNodes = diffNodes' []++diffNodes' :: (?oldHtml :: text, ?newHtml :: Text) => Path -> Node -> Node -> [NodeOperation]+diffNodes' path TextNode { textContent = oldTextContent } TextNode { textContent = newTextContent } =+        if oldTextContent == newTextContent+            then []+            else [UpdateTextContent { textContent = newTextContent, path }]+diffNodes' path CommentNode { comment = oldComment } CommentNode { comment = newComment } =+        if oldComment == newComment+            then []+            else [UpdateComment { comment = newComment, path }]+diffNodes' path NoRenderCommentNode NoRenderCommentNode = []+diffNodes' path PreEscapedTextNode { textContent = oldTextContent } PreEscapedTextNode { textContent = newTextContent } =+        if oldTextContent == newTextContent+            then []+            else [UpdatePreEscapedTextNode { textContent = newTextContent, path }]+diffNodes' path oldNode@(Node { tagName = oldTagName, attributes = oldAttributes, children = oldChildren }) newNode@(Node { tagName = newTagName, attributes = newAttributes, children = newChildren }) =+    if oldTagName == newTagName+        then+            let+                attributeOperations = diffAttributes oldAttributes newAttributes+                childrenNodeOperations = diffNodes' path Children { children = oldChildren } Children { children = newChildren }+            in concat [+                    if isEmpty attributeOperations+                        then []+                        else [UpdateNode { attributeOperations, path }]+                    , childrenNodeOperations+                    ]++        else [ReplaceNode { oldNode, newNode, newNodeHtml = nodeOuterHtml newNode ?newHtml, path }]+diffNodes' path Children { children = oldChildren } Children { children = newChildren } =+        let+            patchElements :: [Node] -> [Node] -> Int -> [NodeOperation]+            patchElements (new:nextNewNode:newRest) (old:oldRest) !index | (not $ new `isNodeEqIgnoringPosition` old) && (old `isNodeEqIgnoringPosition` nextNewNode) = [ CreateNode { html = nodeOuterHtml new ?newHtml, path = (index:path) } ] <> (patchElements (newRest) (oldRest) (index + 2)) -- [A, C <old>] -> [A, B <new>, C <nextNewNode>]+            patchElements (new:newRest) (old:nextOld:oldRest) !index | (not $ new `isNodeEqIgnoringPosition` old) && (new `isNodeEqIgnoringPosition` nextOld) = [ DeleteNode { node = old, path = (index:path) } ] <> (patchElements (newRest) (oldRest) (index + 1)) -- [A, B <old>, C <nextOldNode> ] -> [A, C <new>]+            patchElements (new:newRest) (old:oldRest) !index = (diffNodes' (index:path) old new) <> (patchElements newRest oldRest (index + 1))+            patchElements (new:newRest) [] !index = [ CreateNode { html = nodeOuterHtml new ?newHtml, path = (index:path) } ] <> (patchElements newRest [] (index + 1))+            patchElements [] (old:oldRest) !index = [ DeleteNode { node = old, path = (index:path) } ] <> (patchElements [] oldRest (index + 1))+            patchElements [] [] _ = []+        in+            patchElements newChildren oldChildren 0+diffNodes' path oldNode newNode = [ReplaceNode { oldNode, newNode, newNodeHtml = nodeOuterHtml newNode ?newHtml, path }]+++diffAttributes :: [Attribute] -> [Attribute] -> [AttributeOperation]+diffAttributes old new = addOrUpdateAttributes <> deleteAttributes+    where+        addOrUpdateAttributes :: [AttributeOperation]+        addOrUpdateAttributes =+                new+                |> map (matchAttribute old)+                |> zip new+                |> mapMaybe diffMatchedAttribute++        deleteAttributes :: [AttributeOperation]+        deleteAttributes =+                old+                |> map (matchAttribute new)+                |> zip old+                |> filter (\(_, newAttribute) -> isNothing newAttribute)+                |> map (\(Attribute { attributeName }, _) -> DeleteAttribute { attributeName })++        diffMatchedAttribute :: (Attribute, Maybe Attribute) -> Maybe AttributeOperation+        diffMatchedAttribute (Attribute { attributeName, attributeValue = newValue }, Just Attribute { attributeValue = oldValue }) | newValue == oldValue = Nothing+        diffMatchedAttribute (Attribute { attributeName, attributeValue = newValue }, Just Attribute { attributeValue = oldValue }) = Just UpdateAttribute { attributeName, attributeValue = newValue }+        diffMatchedAttribute (Attribute { attributeName, attributeValue }, Nothing) = Just AddAttribute { attributeName, attributeValue }++        -- | Finds an attribute in 'old' with the same attribute name+        matchAttribute :: [Attribute] -> Attribute -> Maybe Attribute+        matchAttribute attributes Attribute { attributeName } = find (\Attribute { attributeName = attributeName' } -> attributeName == attributeName' ) attributes++-- | Grabs the entire HTML string corresponding to the node boundaries.+--+-- Node boundaries are only stored for 'Node'. Other nodes ('TextNode', etc) don't store start/end offset, so we render+-- them by ourselves.+nodeOuterHtml :: Node -> Text -> Text+nodeOuterHtml Node { startOffset, endOffset } html = html+        |> Text.drop startOffset+        |> Text.take (endOffset - startOffset)+-- Assuming chars are already escaped, because that's what HSX produces+nodeOuterHtml TextNode { textContent } _ = textContent+nodeOuterHtml PreEscapedTextNode { textContent } _ = textContent+nodeOuterHtml Children { children } html = mconcat $ map (`nodeOuterHtml` html) children+nodeOuterHtml CommentNode { comment } _ = "<!--" <> comment <> "-->"+nodeOuterHtml NoRenderCommentNode _ = "" -- HSX comments {- ... -} are not rendered++isNodeEqIgnoringPosition :: Node -> Node -> Bool+isNodeEqIgnoringPosition a@(Node {}) b@(Node {}) = (a { startOffset = 0, endOffset = 0 }) == (b { startOffset = 0, endOffset = 0 })+isNodeEqIgnoringPosition a b = a == b
+ IHP/ServerSideComponent/HtmlParser.hs view
@@ -0,0 +1,134 @@+{-|+Module: IHP.ServerSideComponent.HtmlParser+Copyright: (c) digitally induced GmbH, 2021+Description: Used by serverside DOM diff+-}+module IHP.ServerSideComponent.HtmlParser+( parseHtml+, Node (..)+, Attribute (..)+) where++import CorePrelude+import Text.Megaparsec+import Text.Megaparsec.Char+import Data.Void+import qualified Data.Char as Char+import Data.String.Conversions++data Attribute = Attribute+    { attributeName :: !Text+    , attributeValue :: !Text+    } deriving (Eq, Show)++data Node = Node { tagName :: !Text, attributes :: ![Attribute], children :: ![Node], startOffset :: Int, endOffset :: Int }+    | TextNode { textContent :: !Text } -- ^ Note: doesn't unescape chars like &lt;+    | PreEscapedTextNode { textContent :: !Text } -- ^ Used in @script@ or @style@ bodies+    | Children { children :: ![Node] }+    | CommentNode { comment :: !Text } -- ^ A Comment that is rendered in the final HTML+    | NoRenderCommentNode -- ^ A comment that is not rendered in the final HTML+    deriving (Eq, Show)++parseHtml :: Text -> Either (ParseErrorBundle Text Void) Node+parseHtml code = runParser (parser) "" code++type Parser = Parsec Void Text++parser :: Parser Node+parser = do+    space+    node <- parseChildren <|> parseElement+    space+    eof+    pure node++parseElement = try parseNoRenderComment <|> try parseComment <|> try parseNormalElement <|> try parseSelfClosingElement++parseChildren = Children <$> many parseChild++parseSelfClosingElement = do+    startOffset <- getOffset+    _ <- char '<'+    name <- parseElementName+    attributes <- parseNodeAttributes (string ">" <|> string "/>")+    endOffset <- getOffset+    space+    pure (Node name attributes [] startOffset endOffset)++parseNormalElement = do+    startOffset <- getOffset+    (name, attributes) <- parseOpeningElement+    let parsePreEscapedTextChildren = do+                    let closingElement = "</" <> name <> ">"+                    text <- cs <$> manyTill anySingle (string closingElement)+                    pure [PreEscapedTextNode text]+    let parseNormalChildren = (space >> (manyTill (try parseChild) (try (space >> parseClosingElement name))))++    children <- case name of+            "script" -> parsePreEscapedTextChildren+            "style" -> parsePreEscapedTextChildren+            otherwise -> parseNormalChildren+    endOffset <- getOffset+    pure (Node name attributes children startOffset endOffset)++parseOpeningElement = do+    char '<'+    name <- parseElementName+    space+    attributes <- parseNodeAttributes (char '>')+    pure (name, attributes)++parseComment :: Parser Node+parseComment = do+    string "<!--"+    body :: String <- manyTill (satisfy (const True)) (string "-->")+    space+    pure (CommentNode (cs body))++parseNoRenderComment :: Parser Node+parseNoRenderComment = do+    string "{-"+    manyTill (satisfy (const True)) (string "-}")+    space+    pure NoRenderCommentNode++parseNodeAttributes :: Parser a -> Parser [Attribute]+parseNodeAttributes end = manyTill parseNodeAttribute end++parseNodeAttribute = do+    attributeName <- parseAttributeName+    space++    let parseAttributeValue = do+            _ <- char '='+            space+            attributeValue <- parseQuotedValue+            space+            pure attributeValue++    attributeValue <- fromMaybe "" <$> optional parseAttributeValue+    pure Attribute { attributeName, attributeValue }++parseAttributeName :: Parser Text+parseAttributeName = takeWhile1P Nothing (\c -> Char.isAlphaNum c || c == '-')++parseQuotedValue :: Parser Text+parseQuotedValue = between (char '"') (char '"') (takeWhileP Nothing (\c -> c /= '\"'))++parseClosingElement name = do+    _ <- string ("</" <> name)+    space+    char ('>')+    pure ()++parseChild = parseElement <|> try (parseElement) <|> parseText++parseText :: Parser Node+parseText = TextNode <$> takeWhile1P (Just "text") (\c -> c /= '<' && c /= '>')++parseElementName :: Parser Text+parseElementName = do+    name <- takeWhile1P (Just "identifier") (\c -> Char.isAlphaNum c || c == '_' || c == '-')+    space+    pure name+
+ IHP/ServerSideComponent/RouterFunctions.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-|+Module: IHP.ServerSideComponent.RouterFunctions+Copyright: (c) digitally induced GmbH, 2021+-}+module IHP.ServerSideComponent.RouterFunctions where++import IHP.Prelude+import IHP.ServerSideComponent.Types+import qualified Data.Typeable as Typeable+import qualified Data.ByteString.Char8 as ByteString+import IHP.RouterSupport+import qualified Prelude+import IHP.ServerSideComponent.Controller.ComponentsController ()+import Data.Aeson+import IHP.ControllerSupport++routeComponent :: forall component controller application.+    ( Typeable component+    , Component component controller+    , FromJSON controller+    , InitControllerContext application+    , Typeable application+    , ?application :: application+    , ?request :: Request+    , ?respond :: Respond+    ) => ControllerRoute application+routeComponent = webSocketAppWithCustomPath @(ComponentsController component) @application ("SSC/" <> typeName)+    where+        typeName :: ByteString+        typeName = Typeable.typeOf (error "unreachable" :: component)+                |> Prelude.show+                |> ByteString.pack
+ IHP/ServerSideComponent/Types.hs view
@@ -0,0 +1,45 @@+{-|+Module: IHP.ServerSideComponent.Types+Description: Types & Data Structures for IHP SSC+Copyright: (c) digitally induced GmbH, 2021+-}+module IHP.ServerSideComponent.Types where++import IHP.ViewPrelude+import qualified Network.WebSockets as WebSocket+import GHC.Generics (Generic)++class Component state action | state -> action where+    initialState :: state+    render :: state -> Html+    action ::+        ( ?instanceRef :: IORef (ComponentInstance state)+        , ?connection :: WebSocket.Connection+        , ?context :: ControllerContext+        , ?modelContext :: ModelContext+        ) => state -> action -> IO state++    componentDidMount ::+        ( ?instanceRef :: IORef (ComponentInstance state)+        , ?connection :: WebSocket.Connection+        , ?context :: ControllerContext+        , ?modelContext :: ModelContext+        ) => state -> IO state+    componentDidMount state = pure state++data ComponentsController components+    = ComponentsController+    deriving (Eq, Show, Data)++data ComponentInstance state+    = ComponentInstance { state :: state } -- ^ If you wondered why the current rendered HTML doesn't need to be stored here for later diffing it: As our render functions are pure we can just re-render the HTML based on the state when we want to do our diffing++instance (SetField "state" (ComponentInstance state) state) where+    setField state componentInstance = componentInstance { state }++-- | Error types for SSC operations sent to the client+data SSCError+    = SSCDiffError { errorMessage :: !Text }+    | SSCActionError { errorMessage :: !Text }+    | SSCParseError { errorMessage :: !Text }+    deriving (Eq, Show, Generic)
+ IHP/ServerSideComponent/ViewFunctions.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-|+Module: IHP.ServerSideComponent.ViewFunctions+Copyright: (c) digitally induced GmbH, 2021+-}+module IHP.ServerSideComponent.ViewFunctions where++import IHP.Prelude+import IHP.ViewSupport+import IHP.ServerSideComponent.Types+import IHP.HSX.QQ (hsx)++import qualified Data.Typeable as Typeable++component :: forall component action. (Component component action, Typeable component) => Html+component = componentFromState (initialState @component)++componentFromState :: forall component action. (Component component action, Typeable component) => component -> Html+componentFromState state = [hsx|<div class="ihp-ssc" data-path={path}>{render state}</div>|]+    where+        path = "/SSC/" <> typeName+        typeName = (undefined :: component)+                |> Typeable.typeOf+                |> show
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2020 digitally induced GmbH++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ changelog.md view
@@ -0,0 +1,12 @@+# Changelog for `ihp-ssc`++## v1.5.0++- Add `SSCError` type for structured error reporting (SSCDiffError, SSCActionError, SSCParseError)+- Add proper error handling and logging in SSC controllers using IHP.Log+- Fix missing `NoRenderCommentNode` pattern in `nodeOuterHtml`+- Fix critical bugs and add reconnection support in ihp-ssc.js++## v1.3.0++- Initial release as a standalone package
+ ihp-ssc.cabal view
@@ -0,0 +1,53 @@+cabal-version:       2.2+name:                ihp-ssc+version:             1.5.0+synopsis:            Server Side Components for IHP+description:         IHP Server-Side Components provide a toolkit for building interactive client-side functionality without needing to write too much JavaScript.+license:             MIT+license-file:        LICENSE+author:              digitally induced GmbH+maintainer:          hello@digitallyinduced.com+homepage:            https://ihp.digitallyinduced.com/+bug-reports:         https://github.com/digitallyinduced/ihp/issues+copyright:           (c) digitally induced GmbH+category:            Web, IHP+build-type:          Simple+extra-source-files:  changelog.md++source-repository head+    type:     git+    location: https://github.com/digitallyinduced/ihp++library+    default-language: GHC2021+    default-extensions:+        OverloadedStrings+        , NoImplicitPrelude+        , ImplicitParams+        , DisambiguateRecordFields+        , DuplicateRecordFields+        , OverloadedLabels+        , DataKinds+        , QuasiQuotes+        , TypeFamilies+        , PackageImports+        , RecordWildCards+        , DefaultSignatures+        , FunctionalDependencies+        , PartialTypeSignatures+        , BlockArguments+        , LambdaCase+        , TemplateHaskell+        , OverloadedRecordDot+        , DeepSubsumption+    ghc-options: -Werror=incomplete-patterns -Werror=unused-imports -Werror=missing-fields+    hs-source-dirs: .+    build-depends: ihp, ihp-log, aeson, megaparsec, bytestring, wai, websockets, ihp-hsx, base >= 4.17.0 && < 4.22, string-conversions, basic-prelude, text, blaze-html, attoparsec, wai-request-params+    exposed-modules:+          IHP.ServerSideComponent.Types+        , IHP.ServerSideComponent.ViewFunctions+        , IHP.ServerSideComponent.RouterFunctions+        , IHP.ServerSideComponent.ControllerFunctions+        , IHP.ServerSideComponent.Controller.ComponentsController+        , IHP.ServerSideComponent.HtmlDiff+        , IHP.ServerSideComponent.HtmlParser