on master help sign in
Merge remote-tracking branch 'origin/master' into ts-dont-store-config
commit41c5d9ab8ed31ebe26bd7ade663091849d07292c authorTom Smalley <tfsmalley@gmail.com> dateWed 31 Oct 2018, 10:17 (8 years ago) parent5c2d99a3ec4f82
12 files changed+219−82
README.md+1−1 default.nix+16−10 lib/asset/manifest/src-bin/generate.hs+2−2 lib/backend/obelisk-backend.cabal+1 lib/backend/src/Obelisk/Backend.hs+35−16 lib/command/src/Obelisk/Command/Deploy.hs+23−28 lib/command/src/Obelisk/Command/Thunk.hs+4−3 lib/frontend/src/Obelisk/Frontend.hs+11−8 lib/route/src/Obelisk/Route.hs+2 lib/route/src/Obelisk/Route/Frontend.hs+108−2 lib/run/src/Obelisk/Run.hs+8−6 lib/selftest/src/Obelisk/SelfTest.hs+8−6
README.md+1−1
@@ -140,7 +140,7 @@ ob deploy push
140140
141141`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance. The backend will live in `/var/lib/backend`.
142142
143−At this point you are done. Your app will be accessible at `${ROUTE}`.
143+At this point you are done. Your app will be accessible at `${ROUTE}`. The currently deployed version - the git commit hash of the source repo - can be found at `${ROUTE}/version`.
144144
145145### From macOS
146146
default.nix+16−10
@@ -147,14 +147,12 @@ in rec {
147147 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
148148 inherit src;
149149 outputs = [ "out" "haskellManifest" "symlinked" ];
150− buildInputs = [
151− (reflex-platform.ghc.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {})
152− ];
150+ nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
153151 } ''
154152 set -euo pipefail
155153 touch "$out"
156154 mkdir -p "$symlinked"
157− obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
155+ obelisk-asset-manifest-generate "$src" "${builtins.toString src}" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
158156 '';
159157
160158 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
@@ -232,16 +230,17 @@ in rec {
232230 };
233231 };
234232
235− serverExe = backend: frontend: assets: optimizationLevel:
233+ serverExe = backend: frontend: assets: optimizationLevel: version:
236234 pkgs.runCommand "serverExe" {} ''
237235 mkdir $out
238236 set -eux
239237 ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
240238 ln -s "${mkAssets assets}" $out/static.assets
241239 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
240+ echo ${version} > $out/version
242241 '';
243242
244− server = { exe, hostName, adminEmail, routeHost, enableHttps }@args:
243+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
245244 let
246245 nixos = import (pkgs.path + /nixos);
247246 in nixos {
@@ -327,13 +326,20 @@ in rec {
327326 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles __closureCompilerOptimizationLevel; };
328327 };
329328 in mkProject (projectDefinition args));
330− serverOn = sys: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend (projectOut sys).passthru.staticFiles (projectOut sys).passthru.__closureCompilerOptimizationLevel;
329+ serverOn = sys: version: serverExe
330+ (projectOut sys).ghc.backend
331+ (projectOut system).ghcjs.frontend
332+ (projectOut sys).passthru.staticFiles
333+ (projectOut sys).passthru.__closureCompilerOptimizationLevel
334+ version;
331335 linuxExe = serverOn "x86_64-linux";
336+ dummyVersion = "Version number is only available for deployments";
332337 in projectOut system // {
333338 linuxExeConfigurable = linuxExe;
334− linuxExe = linuxExe;
335− exe = serverOn system;
336− server = args@{ hostName, adminEmail, routeHost, enableHttps }: server (args // { exe = linuxExe; });
339+ linuxExe = linuxExe dummyVersion;
340+ exe = serverOn system dummyVersion;
341+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
342+ server (args // { exe = linuxExe version; });
337343 obelisk = import (base + "/.obelisk/impl") {};
338344 };
339345 haskellPackageSets = {
lib/asset/manifest/src-bin/generate.hs+2−2
@@ -7,11 +7,11 @@ import System.Environment
77main :: IO ()
88main = do
99 --TODO: Usage
10− [root, haskellTarget, packageName, moduleName, fileTarget] <- getArgs
10+ [root, rootLocalPath, haskellTarget, packageName, moduleName, fileTarget] <- getArgs
1111 paths <- gatherHashedPaths root
1212 writeStaticProject paths haskellTarget $ StaticConfig
1313 { _staticConfig_packageName = T.pack packageName
1414 , _staticConfig_moduleName = T.pack moduleName
15− , _staticConfig_rootPath = root
15+ , _staticConfig_rootPath = rootLocalPath
1616 }
1717 copyAndSymlink paths root fileTarget
lib/backend/obelisk-backend.cabal+1
@@ -17,6 +17,7 @@ library
1717 obelisk-executable-config-inject,
1818 obelisk-frontend,
1919 obelisk-route,
20+ obelisk-snap-extras,
2021 reflex-dom,
2122 snap,
2223 snap-server,
lib/backend/src/Obelisk/Backend.hs+35−16
@@ -5,6 +5,7 @@
55{-# LANGUAGE LambdaCase #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
89{-# LANGUAGE TemplateHaskell #-}
910module Obelisk.Backend
1011 ( Backend (..)
@@ -16,6 +17,8 @@ module Obelisk.Backend
1617 , serveDefaultObeliskApp
1718 , prettifyOutput
1819 , runBackend
20+ , staticRenderContentType
21+ , mkRouteToUrl
1922 ) where
2023
2124import Prelude hiding (id, (.))
@@ -37,9 +40,10 @@ import Obelisk.Asset.Serve.Snap (serveAsset)
3740import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
3841import Obelisk.Frontend
3942import Obelisk.Route
43+import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4044import Reflex.Dom
41−import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, rqPathInfo,
42− rqQueryString, writeBS, writeText)
45+import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
46+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
4347import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4448import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4549
@@ -56,8 +60,8 @@ data GhcjsApp route = GhcjsApp
5660
5761-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
5862--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
59−serveDefaultObeliskApp :: MonadSnap m => ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
60−serveDefaultObeliskApp serveStaticAsset frontend = serveObeliskApp serveStaticAsset frontendApp
63+serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
64+serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
6165 where frontendApp = GhcjsApp
6266 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
6367 , _ghcjsApp_value = frontend
@@ -108,16 +112,17 @@ getRouteWith e = do
108112 pageName <- getPageName
109113 return $ tryDecode e pageName
110114
111−serveObeliskApp :: MonadSnap m => ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
112−serveObeliskApp serveStaticAsset frontendApp = \case
113− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
115+serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
116+serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
117+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
114118 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
115119 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
116− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
120+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
117121 ResourceRoute_JSaddleWarp :=> Identity _ -> do
118122 let msg = "Error: Obelisk.Backend received jsaddle request"
119123 liftIO $ putStrLn $ T.unpack msg
120124 writeText msg
125+ ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
121126
122127serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
123128serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
@@ -132,11 +137,15 @@ data GhcjsAppRoute :: (* -> *) -> * -> * where
132137 GhcjsAppRoute_App :: appRouteComponent a -> GhcjsAppRoute appRouteComponent a
133138 GhcjsAppRoute_Resource :: GhcjsAppRoute appRouteComponent [Text]
134139
140+staticRenderContentType :: ByteString
141+staticRenderContentType = "text/html; charset=utf-8"
142+
135143--TODO: Don't assume we're being served at "/"
136−serveGhcjsApp :: MonadSnap m => GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
137−serveGhcjsApp app = \case
138− GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest ->
139− writeBS <=< liftIO $ renderGhcjsFrontend (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
144+serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
145+serveGhcjsApp urlEnc app = \case
146+ GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147+ modifyResponse $ setContentType staticRenderContentType
148+ writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
140149 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
141150
142151runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
@@ -147,10 +156,20 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
147156 getRouteWith validFullEncoder >>= \case
148157 Identity r -> case r of
149158 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
150− InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
159+ InR obeliskRoute :=> Identity a ->
160+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
161+
162+mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
163+mkRouteToUrl validFullEncoder =
164+ let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
165+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
166+
151167
152−renderGhcjsFrontend :: route -> Frontend route -> IO ByteString
153−renderGhcjsFrontend route f = do
168+renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
169+renderGhcjsFrontend urlEnc route f = do
154170 let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
171+ ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
155172 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
156− renderFrontendHtml route (_frontend_head f >> injectExecutableConfigs >> baseTag) (_frontend_body f >> ghcjsScript)
173+ liftIO $ renderFrontendHtml urlEnc route
174+ (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
175+ (_frontend_body f >> ghcjsScript)
lib/command/src/Obelisk/Command/Deploy.hs+23−28
@@ -88,36 +88,31 @@ deployPush deployPath getNixBuilders = do
8888 route <- readDeployConfig deployPath $ "config" </> "common" </> "route"
8989 routeHost <- getHostFromRoute enableHttps route
9090 let srcPath = deployPath </> "src"
91− build = do
92− builders <- getNixBuilders
93− buildOutput <- nixBuild $ def
94− { _nixBuildConfig_target = Target
95− { _target_path = srcPath
96− , _target_attr = Just "server.system"
97− }
98− , _nixBuildConfig_outLink = OutLink_None
99− , _nixBuildConfig_args =
100− [ strArg "hostName" host
101− , strArg "adminEmail" adminEmail
102− , strArg "routeHost" routeHost
103− , boolArg "enableHttps" enableHttps
104− ]
105− , _nixBuildConfig_builders = builders
106− }
107− return $ listToMaybe $ lines buildOutput
108− result <- readThunk srcPath >>= \case
109− Right (ThunkData_Packed _) ->
110− build
91+ thunkPtr <- readThunk srcPath >>= \case
92+ Right (ThunkData_Packed ptr) -> return ptr
11193 Right (ThunkData_Checkout _) -> do
11294 checkGitCleanStatus srcPath True >>= \case
113− True -> do
114− result <- build
115− packThunk srcPath
116− return result
117− False -> do
118− _ <- failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
119− return Nothing
120− _ -> return Nothing
95+ True -> packThunk srcPath
96+ False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
97+ Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
98+ let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
99+ builders <- getNixBuilders
100+ buildOutput <- nixBuild $ def
101+ { _nixBuildConfig_target = Target
102+ { _target_path = srcPath
103+ , _target_attr = Just "server.system"
104+ }
105+ , _nixBuildConfig_outLink = OutLink_None
106+ , _nixBuildConfig_args =
107+ [ strArg "hostName" host
108+ , strArg "adminEmail" adminEmail
109+ , strArg "routeHost" routeHost
110+ , strArg "version" version
111+ , boolArg "enableHttps" enableHttps
112+ ]
113+ , _nixBuildConfig_builders = builders
114+ }
115+ let result = listToMaybe $ lines buildOutput
121116 forM_ result $ \res -> do
122117 let knownHostsPath = deployPath </> "backend_known_hosts"
123118 sshOpts = sshArgs knownHostsPath (deployPath </> "ssh_key") False
lib/command/src/Obelisk/Command/Thunk.hs+4−3
@@ -553,7 +553,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
553553 return tmpThunk
554554 Right _ -> failWith $ "Thunk is not packed"
555555 updateThunkFromTmp p' = do
556− packThunk' True p'
556+ _ <- packThunk' True p'
557557 callProcessAndLogOutput (Notice, Error) $
558558 proc "cp" ["-r", "-T", p', p]
559559
@@ -605,10 +605,10 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkD
605605
606606--TODO: add force mode to pack even if changes are present
607607--TODO: add a rollback mode to pack to the original thunk
608−packThunk :: MonadObelisk m => FilePath -> m ()
608+packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
609609packThunk = packThunk' False
610610
611−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
611+packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
612612packThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
613613 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
614614 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
@@ -618,6 +618,7 @@ packThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir
618618 thunkPtr <- getThunkPtr thunkDir
619619 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
620620 liftIO $ createThunk thunkDir thunkPtr
621+ pure thunkPtr
621622
622623getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
623624getThunkPtr = getThunkPtr' True
lib/frontend/src/Obelisk/Frontend.hs+11−8
@@ -32,6 +32,7 @@ import Data.Dependent.Sum (DSum (..))
3232import Data.Functor.Sum
3333import Data.IORef
3434import Data.Monoid ((<>))
35+import Data.Text (Text)
3536import GHCJS.DOM hiding (bracket, catch)
3637import GHCJS.DOM.Document
3738import GHCJS.DOM.Node
@@ -64,6 +65,7 @@ type ObeliskWidget t x route m =
6465 , PrimMonad m
6566 , Prerender x m
6667 , SetRoute t route m
68+ , RouteToUrl route m
6769 )
6870
6971data Frontend route = Frontend
@@ -143,22 +145,23 @@ runFrontend validFullEncoder frontend = do
143145 , MonadFix m
144146 , MonadFix (Performable m)
145147 )
146− => RoutedT t (R route) (SetRouteT t (R route) m) a
148+ => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
147149 -> m a
148150 runMyRouteViewT = runRouteViewT ve
149151 runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
150− mapRoutedT (mapSetRouteT appendHead) $ _frontend_head frontend
151− mapRoutedT (mapSetRouteT appendBody) $ _frontend_body frontend
152+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
153+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
152154
153155renderFrontendHtml
154156 :: (t ~ DomTimeline)
155− => r
156− -> RoutedT t r (SetRouteT t r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost)))) ()
157− -> RoutedT t r (SetRouteT t r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost)))) ()
157+ => (r' -> Text)
158+ -> r
159+ -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
160+ -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
158161 -> IO ByteString
159−renderFrontendHtml route headWidget bodyWidget = do
162+renderFrontendHtml urlEnc route headWidget bodyWidget = do
160163 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161− html <- fmap snd $ renderStatic $ fmap fst $ runSetRouteT $ flip runRoutedT (pure route) $
164+ html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
162165 el "html" $ do
163166 el "head" headWidget
164167 el "body" bodyWidget
lib/route/src/Obelisk/Route.hs+2
@@ -721,6 +721,7 @@ data ResourceRoute :: * -> * where
721721 ResourceRoute_Static :: ResourceRoute [Text] -- This [Text] represents the *path in our static files directory*, not necessarily the URL path that the asset gets served at (although that will often be "/static/this/text/thing")
722722 ResourceRoute_Ghcjs :: ResourceRoute [Text]
723723 ResourceRoute_JSaddleWarp :: ResourceRoute (R JSaddleWarpRoute)
724+ ResourceRoute_Version :: ResourceRoute ()
724725
725726-- | If there are no additional backend routes in your app (i.e. ObeliskRoute gives you all the routes you need),
726727-- this constructs a suitable 'Encoder' to use for encoding routes to 'PageName's. If you do have additional backend routes,
@@ -757,6 +758,7 @@ resourceRouteSegment = \case
757758 ResourceRoute_Static -> PathSegment "static" $ pathOnlyEncoderIgnoringQuery
758759 ResourceRoute_Ghcjs -> PathSegment "ghcjs" $ pathOnlyEncoder
759760 ResourceRoute_JSaddleWarp -> PathSegment "jsaddle" $ jsaddleWarpRouteEncoder
761+ ResourceRoute_Version -> PathSegment "version" $ unitEncoder mempty
760762
761763data JSaddleWarpRoute :: * -> * where
762764 JSaddleWarpRoute_JavaScript :: JSaddleWarpRoute ()
lib/route/src/Obelisk/Route/Frontend.hs+108−2
@@ -41,6 +41,11 @@ module Obelisk.Route.Frontend
4141 , SetRoute(..)
4242 , runSetRouteT
4343 , mapSetRouteT
44+ , RouteToUrl(..)
45+ , RouteToUrlT(..)
46+ , runRouteToUrlT
47+ , mapRouteToUrlT
48+ , routeLink
4449 ) where
4550
4651import Prelude hiding ((.), id)
@@ -61,6 +66,8 @@ import Data.Constraint (Dict (..))
6166import Data.Dependent.Sum (DSum (..))
6267import Data.GADT.Compare
6368import Data.Monoid
69+import Data.Proxy
70+import Data.Text (Text)
6471import qualified Data.Text as T
6572import Data.Functor.Compose
6673import Reflex.Class
@@ -290,6 +297,85 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
290297 askQueryResult = lift askQueryResult
291298 queryIncremental = lift . queryIncremental
292299
300+class RouteToUrl r m | m -> r where
301+ askRouteToUrl :: m (r -> Text)
302+
303+newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
304+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
305+
306+runRouteToUrlT
307+ :: RouteToUrlT r m a
308+ -> (r -> Text)
309+ -> m a
310+runRouteToUrlT a = runReaderT (unRouteToUrlT a)
311+
312+mapRouteToUrlT :: (forall x. m x -> n x) -> RouteToUrlT r m a -> RouteToUrlT r n a
313+mapRouteToUrlT f (RouteToUrlT m) = RouteToUrlT $ mapReaderT f m
314+
315+instance Monad m => RouteToUrl r (RouteToUrlT r m) where
316+ askRouteToUrl = RouteToUrlT ask
317+
318+instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
319+ askRouteToUrl = lift askRouteToUrl
320+
321+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
322+ askRouteToUrl = lift askRouteToUrl
323+
324+instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
325+ type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
326+ askJSContext = lift askJSContext
327+
328+instance Prerender js m => Prerender js (RouteToUrlT r m) where
329+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
330+
331+instance Requester t m => Requester t (RouteToUrlT r m) where
332+ type Request (RouteToUrlT r m) = Request m
333+ type Response (RouteToUrlT r m) = Response m
334+ requesting = RouteToUrlT . requesting
335+ requesting_ = RouteToUrlT . requesting_
336+
337+#ifndef ghcjs_HOST_OS
338+deriving instance MonadJSM m => MonadJSM (RouteToUrlT r m)
339+#endif
340+
341+instance PerformEvent t m => PerformEvent t (RouteToUrlT r m) where
342+ type Performable (RouteToUrlT r m) = Performable m
343+ performEvent = lift . performEvent
344+ performEvent_ = lift . performEvent_
345+
346+instance MonadRef m => MonadRef (RouteToUrlT r m) where
347+ type Ref (RouteToUrlT r m) = Ref m
348+ newRef = lift . newRef
349+ readRef = lift . readRef
350+ writeRef r = lift . writeRef r
351+
352+instance HasJS x m => HasJS x (RouteToUrlT r m) where
353+ type JSX (RouteToUrlT r m) = JSX m
354+ liftJS = lift . liftJS
355+
356+instance MonadTransControl (RouteToUrlT r) where
357+ type StT (RouteToUrlT r) a = StT (ReaderT (r -> Text)) a
358+ liftWith = defaultLiftWith RouteToUrlT unRouteToUrlT
359+ restoreT = defaultRestoreT RouteToUrlT
360+
361+instance PrimMonad m => PrimMonad (RouteToUrlT r m ) where
362+ type PrimState (RouteToUrlT r m) = PrimState m
363+ primitive = lift . primitive
364+
365+instance DomBuilder t m => DomBuilder t (RouteToUrlT r m) where
366+ type DomBuilderSpace (RouteToUrlT r m) = DomBuilderSpace m
367+
368+instance Adjustable t m => Adjustable t (RouteToUrlT r m) where
369+ runWithReplace a0 a' = RouteToUrlT $ runWithReplace (coerce a0) $ coerceEvent a'
370+ traverseIntMapWithKeyWithAdjust f a0 a' = RouteToUrlT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
371+ traverseDMapWithKeyWithAdjust f a0 a' = RouteToUrlT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
372+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = RouteToUrlT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
373+
374+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
375+ tellQueryIncremental = lift . tellQueryIncremental
376+ askQueryResult = lift askQueryResult
377+ queryIncremental = lift . queryIncremental
378+
293379runRouteViewT
294380 :: forall t m r a.
295381 ( TriggerEvent t m
@@ -300,7 +386,7 @@ runRouteViewT
300386 , MonadFix m
301387 )
302388 => (Encoder Identity Identity r PageName)
303− -> RoutedT t r (SetRouteT t r m) a
389+ -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
304390 -> m a
305391runRouteViewT routeEncoder a = do
306392 rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
@@ -313,7 +399,7 @@ runRouteViewT routeEncoder a = do
313399 where
314400 errorLeft (Left e) = error (T.unpack e)
315401 errorLeft (Right x) = x
316− (result, changeState) <- runSetRouteT $ runRoutedT a route
402+ (result, changeState) <- runRouteToUrlT (runSetRouteT $ runRoutedT a route) $ (\(p, q) -> T.pack $ p <> q) . encode theEncoder
317403 let f (currentHistoryState, oldRoute) change =
318404 let newRoute = appEndo change oldRoute
319405 (newPath, newQuery) = encode theEncoder newRoute
@@ -337,6 +423,26 @@ runRouteViewT routeEncoder a = do
337423 setState = attachWith f ((,) <$> current historyState <*> current route) changeState
338424 return result
339425
426+-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
427+-- contexts, this widget falls back to using @href@s to control navigation
428+routeLink
429+ :: forall t m a route.
430+ ( DomBuilder t m
431+ , RouteToUrl (R route) m
432+ , SetRoute t (R route) m
433+ )
434+ => R route -- ^ Target route
435+ -> m a -- ^ Child widget
436+ -> m a
437+routeLink r w = do
438+ enc <- askRouteToUrl
439+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
440+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
441+ & elementConfig_initialAttributes .~ "href" =: enc r
442+ (e, a) <- element "a" cfg w
443+ setRoute $ r <$ domEvent Click e
444+ return a
445+
340446-- On ios due to sandboxing when loading the page from a file adapt the
341447-- path to be based on the hash.
342448
lib/run/src/Obelisk/Run.hs+8−6
@@ -82,7 +82,8 @@ run port serveStaticAsset backend frontend = do
8282 getRouteWith validFullEncoder >>= \case
8383 Identity r -> case r of
8484 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp serveStaticAsset frontend $ obeliskRoute :/ a
85+ InR obeliskRoute :=> Identity a ->
86+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
8687 let conf = defRunConfig { _runConfig_redirectPort = port }
8788 runWidget conf frontend validFullEncoder `finally` killThread backendTid
8889
@@ -142,14 +143,15 @@ obeliskApp opts frontend validFullEncoder uri backend = do
142143 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
143144 }
144145 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
145− html <- renderJsaddleFrontend (appRouteComponent :/ appRouteRest) frontend
146− sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] $ BSLC.fromStrict html
146+ html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
147+ sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
147148 _ -> backend req sendResponse
148149
149−renderJsaddleFrontend :: route -> Frontend route -> IO ByteString
150−renderJsaddleFrontend r f =
150+renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
151+renderJsaddleFrontend urlEnc r f =
151152 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
152− in renderFrontendHtml r (_frontend_head f >> injectExecutableConfigs) (_frontend_body f >> jsaddleScript)
153+ jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
154+ in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
153155
154156-- | like 'bindPortTCP' but reconnects on exception
155157bindPortTCPRetry :: Settings
lib/selftest/src/Obelisk/SelfTest.hs+8−6
@@ -86,7 +86,9 @@ main = do
8686 let shelly_ = void . shellyOb verbosity
8787
8888 inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
89− inTmp f = shelly_ . withSystemTempDirectory "test" $ (chdir <*> f) . fromString
89+ inTmp f = withTmp (chdir <*> f)
90+
91+ withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
9092
9193 inTmpObInit f = inTmp $ \dir -> do
9294 run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
@@ -136,9 +138,9 @@ main = do
136138 testObRunInDir p2 p3 (Just "frontend") httpManager
137139
138140 describe "obelisk project" $ parallel $ do
139− it "can build obelisk command" $ shelly_ $ run "nix-build" ["-A", "command" , obeliskImpl]
140− it "can build obelisk skeleton" $ shelly_ $ run "nix-build" ["-A", "skeleton", obeliskImpl]
141− it "can build obelisk shell" $ shelly_ $ run "nix-build" ["-A", "shell", obeliskImpl]
141+ it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
142+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
143+ it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
142144 -- See https://github.com/obsidiansystems/obelisk/issues/101
143145 -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
144146
@@ -181,12 +183,12 @@ main = do
181183 assertRevEQ eu eupu
182184 assertRevNE e eu
183185
184− it "unpacks the correct branch" $ inTmp $ \dir -> do
186+ it "unpacks the correct branch" $ withTmp $ \dir -> do
185187 let branch = "master"
186188 run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
187189 run_ "ob" ["thunk", "pack", toTextIgnore dir]
188190 run_ "ob" ["thunk", "unpack", toTextIgnore dir]
189− branch' <- run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
191+ branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
190192 liftIO $ assertEqual "" branch (T.strip branch')
191193
192194 it "can pack and unpack plain git repos" $ do