Browse Source

Add digitallyinduced/ihp to Frameworkbenchmark (#6006)

IHP is a modern batteries-included Web Framework, built on top of Haskell and Nix.
Andreas 4 years ago
parent
commit
7b44454f97
30 changed files with 685 additions and 0 deletions
  1. 1 0
      .travis.yml
  2. 38 0
      frameworks/Haskell/ihp/README.md
  3. 30 0
      frameworks/Haskell/ihp/benchmark_config.json
  4. 19 0
      frameworks/Haskell/ihp/ihp.dockerfile
  5. 3 0
      frameworks/Haskell/ihp/src/.ghci
  6. 19 0
      frameworks/Haskell/ihp/src/.gitignore
  7. 61 0
      frameworks/Haskell/ihp/src/App.cabal
  8. 9 0
      frameworks/Haskell/ihp/src/Application/Helper/Controller.hs
  9. 9 0
      frameworks/Haskell/ihp/src/Application/Helper/View.hs
  10. 9 0
      frameworks/Haskell/ihp/src/Application/Schema.sql
  11. 12 0
      frameworks/Haskell/ihp/src/Application/Script/Prelude.hs
  12. 10 0
      frameworks/Haskell/ihp/src/Config/Config.hs
  13. 0 0
      frameworks/Haskell/ihp/src/Config/nix/haskell-packages/.keep
  14. 90 0
      frameworks/Haskell/ihp/src/Config/nix/nixpkgs-config.nix
  15. 17 0
      frameworks/Haskell/ihp/src/Main.hs
  16. 21 0
      frameworks/Haskell/ihp/src/Makefile
  17. 2 0
      frameworks/Haskell/ihp/src/Setup.hs
  18. 58 0
      frameworks/Haskell/ihp/src/Web/Controller/FrameworkBenchmarks.hs
  19. 12 0
      frameworks/Haskell/ihp/src/Web/Controller/Prelude.hs
  20. 17 0
      frameworks/Haskell/ihp/src/Web/FrontController.hs
  21. 15 0
      frameworks/Haskell/ihp/src/Web/Routes.hs
  22. 26 0
      frameworks/Haskell/ihp/src/Web/Types.hs
  23. 25 0
      frameworks/Haskell/ihp/src/Web/View/Context.hs
  24. 29 0
      frameworks/Haskell/ihp/src/Web/View/FrameworkBenchmarks/Fortune.hs
  25. 32 0
      frameworks/Haskell/ihp/src/Web/View/FrameworkBenchmarks/Index.hs
  26. 65 0
      frameworks/Haskell/ihp/src/Web/View/Layout.hs
  27. 16 0
      frameworks/Haskell/ihp/src/Web/View/Prelude.hs
  28. 23 0
      frameworks/Haskell/ihp/src/default.nix
  29. 17 0
      frameworks/Haskell/ihp/src/start
  30. 0 0
      frameworks/Haskell/ihp/src/static/.keep

+ 1 - 0
.travis.yml

@@ -40,6 +40,7 @@ env:
     - 'TESTDIR="Go/evio Go/fasthttp Go/go-std Go/atreugo Go/gramework"'
     - 'TESTDIR="Go/evio Go/fasthttp Go/go-std Go/atreugo Go/gramework"'
     - 'TESTDIR="Go/gearbox Go/goframe Go/clevergo"'
     - 'TESTDIR="Go/gearbox Go/goframe Go/clevergo"'
     - "TESTLANG=Groovy"
     - "TESTLANG=Groovy"
+    - "TESTDIR=Haskell/ihp"
     - "TESTDIR=Haskell/snap"
     - "TESTDIR=Haskell/snap"
     - "TESTDIR=Haskell/yesod"
     - "TESTDIR=Haskell/yesod"
     - "TESTDIR=Haskell/servant"
     - "TESTDIR=Haskell/servant"

+ 38 - 0
frameworks/Haskell/ihp/README.md

@@ -0,0 +1,38 @@
+# IHP Benchmarking Test
+
+This is the IHP implementation of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+### Test Type Implementation Source Code
+
+* [JSON](src/Web/Controller/FrameworkBenchmarks.hs#15)
+* [DB](src/Web/Controller/FrameworkBenchmarks.hs#18)
+* [QUERY](src/Web/Controller/FrameworkBenchmarks.hs#23)
+* [FORTUNES](src/Web/Controller/FrameworkBenchmarks.hs#32)
+* [UPDATE](src/Web/Controller/FrameworkBenchmarks.hs#39)
+* [PLAINTEXT](src/Web/Controller/FrameworkBenchmarks.hs#52)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 30 - 0
frameworks/Haskell/ihp/benchmark_config.json

@@ -0,0 +1,30 @@
+{
+  "framework": "ihp",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+      	"fortune_url": "/fortunes",
+        "query_url": "/query?queries=",
+        "update_url": "/update?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Fullstack",
+        "database": "postgres",
+        "framework": "IHP",
+        "language": "Haskell",
+        "flavor": "None",
+        "orm": "Full",
+        "platform": "Wai",
+        "webserver": "Wai",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Integrated Haskell Platfrom (IHP)",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 19 - 0
frameworks/Haskell/ihp/ihp.dockerfile

@@ -0,0 +1,19 @@
+FROM nixos/nix
+
+COPY ./src /ihp
+WORKDIR /ihp
+
+# Add build dependencies
+RUN nix-env -i git cachix
+RUN cachix use digitallyinduced
+
+# Build 
+RUN nix-shell -j auto --cores 0 --command "make build/bin/RunOptimizedProdServer"
+
+# Setup
+ENV DATABASE_URL=postgres://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world
+ENV PORT=8080
+EXPOSE 8080
+
+# Run
+CMD nix-shell -j auto --cores 0 --command "./build/bin/RunOptimizedProdServer"

+ 3 - 0
frameworks/Haskell/ihp/src/.ghci

@@ -0,0 +1,3 @@
+import Prelude
+:def source readFile
+:source build/ihp-lib/applicationGhciConfig

+ 19 - 0
frameworks/Haskell/ihp/src/.gitignore

@@ -0,0 +1,19 @@
+.DS_Store
+.envrc
+.idea
+tmp
+result
+node_modules
+bin
+*.iml
+Generated
+dist
+*.dyn_hi
+*.dyn_o
+*.hi
+*.o
+build
+gen
+del
+static/prod.*
+Config/client_session_key.aes

+ 61 - 0
frameworks/Haskell/ihp/src/App.cabal

@@ -0,0 +1,61 @@
+-- Initial App.cabal generated by cabal init.  For
+-- further documentation, see http://haskell.org/cabal/users-guide/
+
+name:                App
+version:             0.1.0.0
+-- synopsis:
+-- description:
+license:             AllRightsReserved
+license-file:        LICENSE
+author:              Marc Scholten
+maintainer:          [email protected]
+-- copyright:
+-- category:
+build-type:          Simple
+cabal-version:       >=1.10
+
+executable App
+  main-is:             Main.hs
+  -- other-modules:
+  -- other-extensions:
+  build-depends:
+  	ihp,
+  	base,
+  	classy-prelude,
+  	directory,
+  	free,
+  	string-conversions,
+  	warp,
+  	wai,
+  	mtl,
+  	blaze-html,
+  	blaze-markup,
+  	wai-extra,
+  	wai,
+  	http-types,
+  	blaze-html,
+  	mtl,
+  	inflections,
+  	text,
+  	postgresql-simple,
+  	wai-middleware-static,
+  	wai-util, http-conduit,
+  	tagsoup,
+  	http-client,
+  	bytestring,
+  	network-uri,
+  	aeson,
+  	wai-session,
+  	wai-session-clientsession,
+  	clientsession,
+  	pwstore-fast,
+  	vault,
+  	data-default,
+  	linklater,
+  	random-strings,
+  	uuid,
+  	time,
+  	parsec
+  hs-source-dirs:      .
+  default-language:    Haskell2010
+  extensions: OverloadedStrings, NoImplicitPrelude, ImplicitParams, Rank2Types, NamedFieldPuns, TypeSynonymInstances, FlexibleInstances, DisambiguateRecordFields, DuplicateRecordFields, OverloadedLabels, FlexibleContexts, DataKinds

+ 9 - 0
frameworks/Haskell/ihp/src/Application/Helper/Controller.hs

@@ -0,0 +1,9 @@
+module Application.Helper.Controller (
+    -- To use the built in login:
+    -- module IHP.LoginSupport.Helper.Controller
+) where
+
+-- Here you can add functions which are available in all your controllers
+
+-- To use the built in login:
+-- import IHP.LoginSupport.Helper.Controller

+ 9 - 0
frameworks/Haskell/ihp/src/Application/Helper/View.hs

@@ -0,0 +1,9 @@
+module Application.Helper.View (
+    -- To use the built in login:
+    -- module IHP.LoginSupport.Helper.View
+) where
+
+-- Here you can add functions which are available in all your views
+
+-- To use the built in login:
+-- import IHP.LoginSupport.Helper.View

+ 9 - 0
frameworks/Haskell/ihp/src/Application/Schema.sql

@@ -0,0 +1,9 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+CREATE TABLE World (
+    id SERIAL PRIMARY KEY NOT NULL,
+    randomnumber INT DEFAULT 0 NOT NULL
+);
+CREATE TABLE Fortune (
+    id SERIAL PRIMARY KEY NOT NULL,
+    message TEXT NOT NULL
+);

+ 12 - 0
frameworks/Haskell/ihp/src/Application/Script/Prelude.hs

@@ -0,0 +1,12 @@
+module Application.Script.Prelude
+( module IHP.ControllerPrelude
+, module Generated.Types
+, module IHP.Prelude
+, module IHP.ScriptSupport
+)
+where
+
+import IHP.Prelude
+import IHP.ControllerPrelude
+import Generated.Types
+import IHP.ScriptSupport

+ 10 - 0
frameworks/Haskell/ihp/src/Config/Config.hs

@@ -0,0 +1,10 @@
+module Config where
+
+import IHP.Prelude
+import IHP.Environment
+import IHP.FrameworkConfig
+
+instance FrameworkConfig where 
+    environment = Production
+    appHostname = "localhost"
+    requestLoggerMiddleware = \application -> application

+ 0 - 0
frameworks/Haskell/ihp/src/Config/nix/haskell-packages/.keep


+ 90 - 0
frameworks/Haskell/ihp/src/Config/nix/nixpkgs-config.nix

@@ -0,0 +1,90 @@
+{ ihp }:
+
+let
+  dontCheckPackages = [
+    "ghc-mod"
+    "cabal-helper"
+    "generic-lens"
+    "filesystem-conduit"
+    "tz"
+    "typerep-map"
+  ];
+
+  doJailbreakPackages = [
+    "ghc-mod"
+    "filesystem-conduit"
+    "http-media"
+  ];
+
+  dontHaddockPackages = [];
+
+  nixPkgsRev = "da7ddd822e32aeebea00e97ab5aeca9758250a40";
+  nixPkgsSha256 = "0zbxbk4m72psbvd5p4qprcpiadndq1j2v517synijwp2vxc7cnv6";
+
+  compiler = "ghc883";
+
+  generatedOverrides = haskellPackagesNew: haskellPackagesOld:
+    let
+      toPackage = dir: file: _: {
+        name = builtins.replaceStrings [ ".nix" ] [ "" ] file;
+
+        value = haskellPackagesNew.callPackage ("${dir}/${file}") {};
+      };
+      makePackageSet = dir: pkgs.lib.mapAttrs' (toPackage dir) (builtins.readDir dir);
+    in
+      { "ihp" = ((haskellPackagesNew.callPackage "${ihp}/ihp.nix") { }); } // (makePackageSet ./haskell-packages/.) // (makePackageSet "${ihp}/NixSupport/haskell-packages/.");
+
+  makeOverrides =
+    function: names: haskellPackagesNew: haskellPackagesOld:
+      let
+        toPackage = name: {
+          inherit name;
+
+          value = function haskellPackagesOld.${name};
+        };
+      in
+      builtins.listToAttrs (map toPackage names);
+
+  composeExtensionsList = pkgs.lib.fold pkgs.lib.composeExtensions (_: _: {});
+
+  # More exotic overrides go here
+  manualOverrides = haskellPackagesNew: haskellPackagesOld: {
+    ihp = pkgs.haskell.lib.allowInconsistentDependencies haskellPackagesOld.ihp;
+    time_1_9_3 = pkgs.haskell.lib.dontCheck haskellPackagesOld.time_1_9_3;
+  };
+
+  #mkDerivation = args: super.mkDerivation (args // {
+  #    enableLibraryProfiling = true;
+  #});
+  config = {
+    allowBroken = true;
+    packageOverrides = pkgs: rec {
+      haskell = pkgs.haskell // {
+        packages = pkgs.haskell.packages // {
+          "${compiler}" =
+          pkgs.haskell.packages."${compiler}".override {
+            overrides = composeExtensionsList [
+              generatedOverrides
+              (makeOverrides pkgs.haskell.lib.dontCheck   dontCheckPackages  )
+              (makeOverrides pkgs.haskell.lib.doJailbreak doJailbreakPackages)
+              (makeOverrides pkgs.haskell.lib.dontHaddock dontHaddockPackages)
+              manualOverrides
+            ];
+          };
+        }
+        ;
+      }
+      ;
+    };
+  };
+
+
+  pkgs = (import ((import <nixpkgs> {}).fetchFromGitHub {
+    owner = "NixOS";
+    repo = "nixpkgs";
+    rev = nixPkgsRev;
+    sha256 = nixPkgsSha256;
+  })) { inherit config; };
+
+in
+pkgs

+ 17 - 0
frameworks/Haskell/ihp/src/Main.hs

@@ -0,0 +1,17 @@
+module Main where
+import IHP.Prelude
+
+import Config
+import qualified IHP.Server
+import IHP.RouterSupport
+import IHP.FrameworkConfig
+import Web.FrontController
+import Web.Types
+
+instance FrontController RootApplication where
+    controllers = [
+            mountFrontController WebApplication
+        ]
+
+main :: IO ()
+main = IHP.Server.run

+ 21 - 0
frameworks/Haskell/ihp/src/Makefile

@@ -0,0 +1,21 @@
+ifneq ($(wildcard IHP/.*),)
+IHP = IHP/lib/IHP
+else
+IHP = $(shell dirname $$(which RunDevServer))/../lib/IHP
+endif
+
+include ${IHP}/Makefile.dist
+
+CSS_FILES += ${IHP}/static/vendor/bootstrap.min.css
+CSS_FILES += ${IHP}/static/vendor/flatpickr.min.css
+
+JS_FILES += ${IHP}/static/vendor/jquery-3.2.1.slim.min.js
+JS_FILES += ${IHP}/static/vendor/timeago.js
+JS_FILES += ${IHP}/static/vendor/popper.min.js
+JS_FILES += ${IHP}/static/vendor/bootstrap.min.js
+JS_FILES += ${IHP}/static/vendor/flatpickr.js
+JS_FILES += ${IHP}/static/helpers.js
+JS_FILES += ${IHP}/static/vendor/morphdom-umd.min.js
+JS_FILES += ${IHP}/static/vendor/turbolinks.js
+JS_FILES += ${IHP}/static/vendor/turbolinksInstantClick.js
+JS_FILES += ${IHP}/static/vendor/turbolinksMorphdom.js

+ 2 - 0
frameworks/Haskell/ihp/src/Setup.hs

@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain

+ 58 - 0
frameworks/Haskell/ihp/src/Web/Controller/FrameworkBenchmarks.hs

@@ -0,0 +1,58 @@
+module Web.Controller.FrameworkBenchmarks where
+
+import Web.Controller.Prelude
+import Web.View.FrameworkBenchmarks.Fortune
+import System.Random
+import qualified Control.Concurrent.Async as Async
+
+instance ToJSON World where
+  toJSON world = object [("id" .= get #id world), ("randomNumber" .= get #randomnumber world)]
+
+instance ToJSON Fortune where
+  toJSON fortune = object ["message" .= get #message fortune]
+
+instance Controller FrameworkBenchmarksController where
+    action JsonAction = do
+        renderJson (toJSON (object ["message" .= ("Hello, World!" :: Text)]))
+    
+    action DbAction = do
+        randomNumber :: Int <- randomRIO (1, 10000)
+        randomWorld <- query @World |> findBy #id (Id randomNumber)
+        renderJson (toJSON randomWorld)
+
+    action QueryAction = do
+        let queries = paramOrDefault @Int 1 "queries" |> toBoundaries
+        let fetchRandomWorld i = do
+                randomWorldId :: Id World <- Id <$> randomRIO (1, 10000)
+                fetch randomWorldId
+        [1..queries]
+            |> Async.mapConcurrently fetchRandomWorld
+            >>= renderJson
+    
+    action FortuneAction = do
+        allFortunes :: [Fortune] <- query @Fortune |> fetch
+        let newFortune :: Fortune = newRecord @Fortune |> set #message "Additional fortune added at request time."
+        let compareFortunes :: Fortune -> Fortune -> Ordering = \f1 f2 -> compare (get #message f1) (get #message f2)
+        let fortunes :: [Fortune] = sortBy compareFortunes (newFortune:allFortunes)
+        renderHtml FortuneView { .. } >>= respondHtml
+
+    action UpdatesAction = do
+        let queries = paramOrDefault @Int 1 "queries" |> toBoundaries
+        let updateRandomWorld i = do
+                randomWorldId :: Id World <- Id <$> randomRIO (1, 10000)
+                newRandom :: Int <- randomRIO (1, 10000)
+                world <- fetch randomWorldId
+                world 
+                    |> set #randomnumber newRandom
+                    |> updateRecord
+        [1..queries]
+            |> Async.mapConcurrently updateRandomWorld
+            >>= renderJson
+    
+    action PlaintextAction = do
+        renderPlain "Hello, World!"
+    
+toBoundaries n
+    | n < 1 = 1
+    | n > 500 = 500
+    | otherwise = n

+ 12 - 0
frameworks/Haskell/ihp/src/Web/Controller/Prelude.hs

@@ -0,0 +1,12 @@
+module Web.Controller.Prelude
+( module Web.Types
+, module Application.Helper.Controller
+, module IHP.ControllerPrelude
+, module Generated.Types
+)
+where
+
+import Web.Types
+import Application.Helper.Controller
+import IHP.ControllerPrelude
+import Generated.Types

+ 17 - 0
frameworks/Haskell/ihp/src/Web/FrontController.hs

@@ -0,0 +1,17 @@
+module Web.FrontController where
+import IHP.RouterPrelude
+import IHP.ControllerSupport
+import Generated.Types
+import Web.Types
+
+-- Controller Imports
+import Web.Controller.FrameworkBenchmarks
+import IHP.Welcome.Controller
+
+instance FrontController WebApplication where
+    controllers = 
+        [ parseRoute @FrameworkBenchmarksController
+        -- Generator Marker
+        ]
+
+instance InitControllerContext WebApplication

+ 15 - 0
frameworks/Haskell/ihp/src/Web/Routes.hs

@@ -0,0 +1,15 @@
+module Web.Routes where
+import IHP.RouterPrelude
+import Generated.Types
+import Web.Types
+
+-- Generator Marker
+instance CanRoute FrameworkBenchmarksController where
+    parseRoute' = (string "/json" <* endOfInput >> pure JsonAction)
+        <|> (string "/plaintext" <* endOfInput >> pure PlaintextAction)
+        <|> (string "/db" <* endOfInput >> pure DbAction)
+        <|> (string "/fortunes" <* endOfInput >> pure FortuneAction)
+        <|> (string "/query" <* endOfInput >> pure QueryAction)
+        <|> (string "/update" <* endOfInput >> pure UpdatesAction)
+
+instance HasPath FrameworkBenchmarksController

+ 26 - 0
frameworks/Haskell/ihp/src/Web/Types.hs

@@ -0,0 +1,26 @@
+module Web.Types where
+import IHP.Prelude
+import qualified IHP.Controller.Session
+import qualified IHP.ControllerSupport as ControllerSupport
+import IHP.ModelSupport
+import Application.Helper.Controller
+import IHP.ViewSupport
+import Generated.Types
+
+data WebApplication = WebApplication deriving (Eq, Show)
+
+data ViewContext = ViewContext
+    { requestContext :: ControllerSupport.RequestContext
+    , flashMessages :: [IHP.Controller.Session.FlashMessage]
+    , controllerContext :: ControllerSupport.ControllerContext
+    , layout :: Layout
+    }
+
+data FrameworkBenchmarksController
+    = JsonAction
+    | PlaintextAction
+    | DbAction
+    | QueryAction
+    | FortuneAction
+    | UpdatesAction
+    deriving (Eq, Show, Data)

+ 25 - 0
frameworks/Haskell/ihp/src/Web/View/Context.hs

@@ -0,0 +1,25 @@
+module Web.View.Context where
+
+import IHP.Prelude
+import qualified IHP.Controller.Session
+import IHP.ControllerSupport  (RequestContext (RequestContext))
+import qualified IHP.ControllerSupport
+import IHP.ModelSupport
+import Application.Helper.Controller
+import Generated.Types
+import qualified IHP.ViewSupport as ViewSupport
+import Web.View.Layout
+import Web.Types
+
+instance ViewSupport.CreateViewContext ViewContext where
+    type ViewApp ViewContext = WebApplication
+    createViewContext = do
+        flashMessages <- IHP.Controller.Session.getAndClearFlashMessages
+        let viewContext = ViewContext {
+                requestContext = ?requestContext,
+                -- user = currentUserOrNothing,
+                flashMessages,
+                controllerContext = ?controllerContext,
+                layout = let ?viewContext = viewContext in defaultLayout
+            }
+        pure viewContext

+ 29 - 0
frameworks/Haskell/ihp/src/Web/View/FrameworkBenchmarks/Fortune.hs

@@ -0,0 +1,29 @@
+module Web.View.FrameworkBenchmarks.Fortune where
+import Web.View.Prelude
+
+data FortuneView = FortuneView { fortunes :: [Fortune] }
+
+instance View FortuneView ViewContext where
+    beforeRender (context, view) = (context { layout = \v -> v }, view)
+    html FortuneView { .. } = preEscapedToHtml ("<!DOCTYPE html>" :: Text) <> [hsx|
+        <html>
+            <head><title>Fortunes</title></head>
+            <body>
+                <table>
+                    <tr>
+                        <th>id</th>
+                        <th>message</th>
+                    </tr>
+                    {forEach fortunes renderFortunes}
+                </table>
+            </body>
+        </html>
+    |]
+
+
+renderFortunes fortune = [hsx|
+    <tr>
+        <td>{get #id fortune}</td>
+        <td>{get #message fortune}</td>
+    </tr>
+|]

+ 32 - 0
frameworks/Haskell/ihp/src/Web/View/FrameworkBenchmarks/Index.hs

@@ -0,0 +1,32 @@
+module Web.View.FrameworkBenchmark.Index where
+import Web.View.Prelude
+
+data FortuneView = FortuneView { fortunes :: [Fortune] }
+
+instance View FortuneView ViewContext where
+    html FortuneView { .. } = [hsx|
+        <nav>
+            <ol class="breadcrumb">
+            </ol>
+        </nav>
+        
+        <div class="table-responsive">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th>FrameworkBenchmark</th>
+                        <th></th>
+                        <th></th>
+                        <th></th>
+                    </tr>
+                </thead>
+            </table>
+        </div>
+    |]
+
+
+renderFrameworkBenchmark frameworkBenchmark = [hsx|
+    <tr>
+        <td>{frameworkBenchmark}</td>
+    </tr>
+|]

+ 65 - 0
frameworks/Haskell/ihp/src/Web/View/Layout.hs

@@ -0,0 +1,65 @@
+module Web.View.Layout (defaultLayout, Html) where
+
+import IHP.ViewPrelude
+import IHP.Environment
+import qualified Text.Blaze.Html5            as H
+import qualified Text.Blaze.Html5.Attributes as A
+import Web.Types
+import Web.Routes
+import qualified IHP.FrameworkConfig as FrameworkConfig
+import Config ()
+
+type Html = HtmlWithContext ViewContext
+
+defaultLayout :: Html -> Html
+defaultLayout inner = H.docTypeHtml ! A.lang "en" $ [hsx|
+<head>
+    {metaTags}
+
+    {stylesheets}
+    {scripts}
+
+    <title>App</title>
+</head>
+<body>
+    <div class="container mt-4">
+        {renderFlashMessages}
+        {inner}
+    </div>
+</body>
+|]
+
+stylesheets = do
+    when (isDevelopment FrameworkConfig.environment) [hsx|
+        <link rel="stylesheet" href="/vendor/bootstrap.min.css"/>
+        <link rel="stylesheet" href="/vendor/flatpickr.min.css"/>
+        <link rel="stylesheet" href="/app.css"/>
+    |]
+    when (isProduction FrameworkConfig.environment) [hsx|
+        <link rel="stylesheet" href="/prod.css"/>
+    |]
+
+scripts = do
+    when (isDevelopment FrameworkConfig.environment) [hsx|
+        <script id="livereload-script" src="/livereload.js"></script>
+        <script src="/vendor/jquery-3.2.1.slim.min.js"></script>
+        <script src="/vendor/timeago.js"></script>
+        <script src="/vendor/popper.min.js"></script>
+        <script src="/vendor/bootstrap.min.js"></script>
+        <script src="/vendor/flatpickr.js"></script>
+        <script src="/helpers.js"></script>
+        <script src="/vendor/morphdom-umd.min.js"></script>
+    |]
+    when (isProduction FrameworkConfig.environment) [hsx|
+        <script src="/prod.js"></script>
+    |]
+
+
+metaTags = [hsx|
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <meta property="og:title" content="App"/>
+    <meta property="og:type" content="website"/>
+    <meta property="og:url" content="TODO"/>
+    <meta property="og:description" content="TODO"/>
+|]

+ 16 - 0
frameworks/Haskell/ihp/src/Web/View/Prelude.hs

@@ -0,0 +1,16 @@
+module Web.View.Prelude
+( module IHP.ViewPrelude
+, module Web.View.Layout
+, module Generated.Types
+, module Web.Types
+, module Web.View.Context
+, module Application.Helper.View
+) where
+
+import IHP.ViewPrelude
+import Web.View.Layout
+import Generated.Types
+import Web.Types
+import Web.Routes ()
+import Web.View.Context
+import Application.Helper.View

+ 23 - 0
frameworks/Haskell/ihp/src/default.nix

@@ -0,0 +1,23 @@
+let
+    ihp = builtins.fetchGit {
+        url = "https://github.com/digitallyinduced/ihp.git";
+        rev = "a12a1ce8f16814b802aae39eb26a9d3247192c12";
+    };
+    haskellEnv = import "${ihp}/NixSupport/default.nix" {
+        ihp = ihp;
+        haskellDeps = p: with p; [
+            cabal-install
+            base
+            wai
+            text
+            hlint
+            p.ihp
+        ];
+        otherDeps = p: with p; [
+	    git
+            # Native dependencies, e.g. imagemagick
+        ];
+        projectPath = ./.;
+    };
+in
+    haskellEnv

+ 17 - 0
frameworks/Haskell/ihp/src/start

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Script to start the local dev server
+
+set -e
+
+# Unless the RunDevServer binary is available, we rebuild the .envrc cache with nix-shell
+# and config cachix for using our binary cache
+command -v RunDevServer >/dev/null 2>&1 \
+    || { echo "PATH_add $(nix-shell -j auto --cores 0 --run 'printf %q $PATH')" > .envrc; }
+
+# Now we have to load the PATH variable from the .envrc cache
+direnv allow
+eval "$(direnv hook bash)"
+eval "$(direnv export bash)"
+
+# Finally start the dev server
+RunDevServer

+ 0 - 0
frameworks/Haskell/ihp/src/static/.keep