on master help sign in
Merge remote-tracking branch 'origin/develop' into add-obelisk-profiling-feature
commitab48708975d23e760740d347dbf3b852cd0c4873 authorMatthew Bauer <mjbauer95@gmail.com> dateTue 11 Feb 2020, 12:15 (7 years ago) parent0be7a17cb654e4
9 files changed+157−49
.github/pull_request_template.md+1−1 ChangeLog.md+6 LICENSE+30 README.md+4 lib/command/obelisk-command.cabal+1−2 lib/command/src/Obelisk/Command/Run.hs+13−16 lib/command/src/Obelisk/Command/Thunk.hs+22−2 lib/route/src/Obelisk/Route/Frontend.hs+80−16 lib/selftest/src/Obelisk/SelfTest.hs−12
.github/pull_request_template.md+1−1
@@ -1,4 +1,4 @@
1−[Provide a clear overview of your changes.]
1+<!-- Provide a clear overview of your changes. -->
22
33I have:
44
ChangeLog.md+6
@@ -4,10 +4,16 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7+* Fix a bug in `Obelisk.Route.Frontend` where `routeLink`, `routeLinkDynAttr`, and `dynRouteLink` would not behave exactly like `<a href="...">` when run by JavaScript. These functions now scroll to the top of the page when the link is clicked. ([#540](https://github.com/obsidiansystems/obelisk/pull/540))
8+
9+## v0.5.0.0 - 2020-02-07
10+
711* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
812* Add local unpacked packages to the `ob run`, `ob watch`, and `ob repl` sessions. Any `.cabal` or hpack package inside the current obelisk project will be loaded into the session. For `ob run`/`ob watch` this means the session will automatically reload when you save a source file in any of those packages. For `ob repl` it means that `:r` will reload changes to any of those packages. There are some edge cases where this integration is still rough. Report any issues you encounter. ([#489](https://github.com/obsidiansystems/obelisk/pull/489))
913* Add `ob hoogle` command to start a local [Hoogle](https://hoogle.haskell.org/) server for the project. ([#628](https://github.com/obsidiansystems/obelisk/pull/628))
1014* `ob thunk pack` will now attempt to automatically detect if the thunk is a private or public repo. To avoid this detection, specify `--private` or `--public` manually. ([#607](https://github.com/obsidiansystems/obelisk/pull/607))
15+* Fix a bug in the plain git thunk loader for thunks marked as 'private' when the revision is not in the default branch. ([#648](https://github.com/obsidiansystems/obelisk/pull/648))
16+* Improve handling of runtime nix dependencies. This may fix some issues encountered particularly by users on systems other than NixOS.
1117* Add `ob profile` command to run Obelisk project with profiling. `ob
1218profile` works like ob run, but instead of using ghci, it builds an
1319executable that is built with profiling enabled.
LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2020 Obsidian Systems
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Elliot Cameron nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md+4
@@ -1,5 +1,9 @@
11# Obelisk
22
3+[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)
4+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)
5+
6+
37Obelisk provides an easy way to develop and deploy your [Reflex](https://github.com/reflex-frp/reflex) project as web apps and as mobile apps.
48
59- [Installing Obelisk](#installing-obelisk)
lib/command/obelisk-command.cabal+1−2
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.4
2+version: 0.5
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -20,7 +20,6 @@ library
2020 , either
2121 , errors
2222 , exceptions
23− , extra
2423 , filepath
2524 , git
2625 , github
lib/command/src/Obelisk/Command/Run.hs+13−16
@@ -15,7 +15,6 @@ import Control.Monad.Reader (MonadIO)
1515import Data.Coerce (coerce)
1616import Data.Either
1717import Data.Foldable (for_, toList)
18−import Data.List.Extra (dropPrefix)
1918import qualified Data.List.NonEmpty as NE
2019import Data.Map (Map)
2120import qualified Data.Map as Map
@@ -53,8 +52,7 @@ import System.Process (waitForProcess)
5352
5453import Obelisk.App (MonadObelisk)
5554import Obelisk.CliApp (Severity (..) , failWith, putLog, proc, readCreateProcessWithExitCode, setCwd, setDelegateCtlc, createProcess_)
56−import Obelisk.Command.Project (obeliskDirName, toObeliskDir, withProjectRoot, nixShellWithPkgs, findProjectAssets)
57−import Obelisk.Command.Thunk (attrCacheFileName)
55+import Obelisk.Command.Project (withProjectRoot, nixShellWithPkgs, findProjectAssets)
5856import Obelisk.Command.Utils (findExePath, ghcidExePath)
5957
6058data CabalPackageInfo = CabalPackageInfo
@@ -162,20 +160,19 @@ runWatch = withProjectRoot "." $ \root -> do
162160-- to the Nix @project@ function.
163161getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
164162getLocalPkgs root = do
165− (_exitCode, out, err) <- readCreateProcessWithExitCode $
166− proc findExePath ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
167− putLog Debug $ T.strip $ T.pack err
163+ obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
168164
169− let
170− -- We ignore any path that has ".obelisk" in it, but keep the root ".obelisk" paths
171− packagePaths = filter (not . isIgnored) $ map T.unpack $ T.lines $ T.strip $ T.pack out
172− isIgnored path =
173− obeliskDirName `elem` dropPrefix (splitPath $ toObeliskDir root) pathSegments
174− && attrCacheFileName `notElem` pathSegments
175− where
176− pathSegments = splitPath path
177−
178− pure packagePaths
165+ -- We do not want to find packages that are embedded inside other obelisk projects, unless that
166+ -- obelisk project is our own.
167+ let exclusions = filter (/= root) $ map takeDirectory obeliskPaths
168+ runFind $
169+ ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
170+ <> concat [["-not", "-path", p </> "*"] | p <- exclusions]
171+ where
172+ runFind args = do
173+ (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
174+ putLog Debug $ T.strip $ T.pack err
175+ pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
179176
180177data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
181178 deriving (Eq, Ord, Show)
lib/command/src/Obelisk/Command/Thunk.hs+22−2
@@ -513,9 +513,10 @@ gitHubStandaloneLoaderV4 = T.unlines
513513
514514plainGitStandaloneLoaders :: NonEmpty Text
515515plainGitStandaloneLoaders =
516− plainGitStandaloneLoaderV3 :|
516+ plainGitStandaloneLoaderV4 :|
517517 [ plainGitStandaloneLoaderV1
518518 , plainGitStandaloneLoaderV2
519+ , plainGitStandaloneLoaderV3
519520 ]
520521
521522plainGitStandaloneLoaderV1 :: Text
@@ -536,6 +537,8 @@ plainGitStandaloneLoaderV2 = T.unlines
536537 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
537538 ]
538539
540+-- This loader has a bug because @builtins.fetchGit@ is not given a @ref@
541+-- and will fail to find commits without this because it does shallow clones.
539542plainGitStandaloneLoaderV3 :: Text
540543plainGitStandaloneLoaderV3 = T.unlines
541544 [ "# DO NOT HAND-EDIT THIS FILE"
@@ -552,6 +555,23 @@ plainGitStandaloneLoaderV3 = T.unlines
552555 , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
553556 ]
554557
558+plainGitStandaloneLoaderV4 :: Text
559+plainGitStandaloneLoaderV4 = T.unlines
560+ [ "# DO NOT HAND-EDIT THIS FILE"
561+ , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
562+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
563+ , " if firstChar == \"/\" then /. + url"
564+ , " else if firstChar == \".\" then ./. + url"
565+ , " else url;"
566+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
567+ , " url = realUrl; inherit rev;"
568+ , " ${if branch == null then null else \"ref\"} = branch;"
569+ , " } else (import <nixpkgs> {}).fetchgit {"
570+ , " url = realUrl; inherit rev sha256;"
571+ , " };"
572+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
573+ ]
574+
555575thunkFileNames :: [FilePath]
556576thunkFileNames = ["default.nix", "github.json", "git.json"]
557577
@@ -937,7 +957,7 @@ guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
937957 urisToTry = nubOrd $
938958 -- Include the original URI if it isn't using SSH because SSH will certainly fail.
939959 [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
940− [changeScheme "https" uri, changeScheme "http" uri, changeScheme "git" uri]
960+ [changeScheme "https" uri, changeScheme "http" uri]
941961 changeScheme scheme (GitUri u) = GitUri $ u
942962 { URI.uriScheme = URI.mkScheme scheme
943963 , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
lib/route/src/Obelisk/Route/Frontend.hs+80−16
@@ -7,15 +7,11 @@
77{-# LANGUAGE FlexibleContexts #-}
88{-# LANGUAGE FlexibleInstances #-}
99{-# LANGUAGE GADTs #-}
10−{-# LANGUAGE KindSignatures #-}
1110{-# LANGUAGE LambdaCase #-}
12−{-# LANGUAGE MultiParamTypeClasses #-}
1311{-# LANGUAGE OverloadedStrings #-}
1412{-# LANGUAGE PatternSynonyms #-}
1513{-# LANGUAGE RankNTypes #-}
1614{-# LANGUAGE ScopedTypeVariables #-}
17−{-# LANGUAGE TemplateHaskell #-}
18−{-# LANGUAGE TupleSections #-}
1915{-# LANGUAGE TypeFamilies #-}
2016{-# LANGUAGE TypeOperators #-}
2117{-# LANGUAGE ViewPatterns #-}
@@ -64,7 +60,7 @@ import Prelude hiding ((.), id)
6460import Obelisk.Route
6561
6662import Control.Category (Category (..), (.))
67−import Control.Category.Cartesian
63+import Control.Category.Cartesian ((&&&))
6864import Control.Lens hiding (Bifunctor, bimap, universe, element)
6965import Control.Monad ((<=<))
7066import Control.Monad.Fix
@@ -94,7 +90,9 @@ import Reflex.Dom.Builder.Class
9490import Data.Type.Coercion
9591import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
9692import Reflex.Dom.Core
93+import qualified GHCJS.DOM as DOM
9794import qualified GHCJS.DOM.Types as DOM
95+import qualified GHCJS.DOM.Window as Window
9896import Network.URI
9997import Data.Maybe (fromMaybe)
10098import qualified Data.List as L
@@ -511,59 +509,125 @@ runRouteViewT routeEncoder switchover useHash a = do
511509-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
512510-- contexts, this widget falls back to using @href@s to control navigation
513511routeLink
514− :: forall t m a route.
512+ :: forall t m a route js.
515513 ( DomBuilder t m
516514 , RouteToUrl route m
517515 , SetRoute t route m
516+ , Prerender js t m
518517 )
519518 => route -- ^ Target route
520519 -> m a -- ^ Child widget
521520 -> m a
522521routeLink r w = do
522+ (e, a) <- routeLinkImpl r w
523+ scrollToTop e
524+ return a
525+
526+-- | Raw implementation of 'routeLink'. Does not scroll to the top of the page on clicks.
527+routeLinkImpl
528+ :: forall t m a route.
529+ ( DomBuilder t m
530+ , RouteToUrl route m
531+ , SetRoute t route m
532+ )
533+ => route -- ^ Target route
534+ -> m a -- ^ Child widget
535+ -> m (Event t (), a)
536+routeLinkImpl r w = do
523537 enc <- askRouteToUrl
524538 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
525539 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
526540 & elementConfig_initialAttributes .~ "href" =: enc r
527541 (e, a) <- element "a" cfg w
528542 setRoute $ r <$ domEvent Click e
529− return a
543+ return (domEvent Click e, a)
544+
545+scrollToTop :: forall m t js. (Prerender js t m, Monad m) => Event t () -> m ()
546+scrollToTop e = prerender_ blank $ performEvent_ $ ffor e $ \_ -> liftJSM $ DOM.currentWindow >>= \case
547+ Nothing -> pure ()
548+ Just win -> Window.scrollTo win 0 0
530549
531550-- | Like 'routeLinkDynAttr' but without custom attributes.
532551dynRouteLink
533− :: forall t m a route.
552+ :: forall t m a route js.
534553 ( DomBuilder t m
535554 , PostBuild t m
536− , RouteToUrl (R route) m
537− , SetRoute t (R route) m
555+ , RouteToUrl route m
556+ , SetRoute t route m
557+ , Prerender js t m
538558 )
539− => Dynamic t (R route) -- ^ Target route
540− -> m a -- ^ Child widget of the @a@ element
559+ => Dynamic t route -- ^ Target route
560+ -> m a -- ^ Child widget
541561 -> m a
542−dynRouteLink = routeLinkDynAttr mempty
562+dynRouteLink r w = do
563+ (e, a) <- dynRouteLinkImpl r w
564+ scrollToTop e
565+ return a
566+
567+-- | Raw implementation of 'dynRouteLink'. Does not scroll to the top of the page on clicks.
568+dynRouteLinkImpl
569+ :: forall t m a route.
570+ ( DomBuilder t m
571+ , PostBuild t m
572+ , RouteToUrl route m
573+ , SetRoute t route m
574+ )
575+ => Dynamic t route -- ^ Target route
576+ -> m a -- ^ Child widget
577+ -> m (Event t (), a)
578+dynRouteLinkImpl dr w = do
579+ enc <- askRouteToUrl
580+ er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
581+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
582+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
583+ & elementConfig_modifyAttributes .~ er
584+ (e, a) <- element "a" cfg w
585+ let clk = domEvent Click e
586+ setRoute $ tag (current dr) clk
587+ return (clk, a)
543588
544589-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
545590-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
546591-- build is used so the link still works like 'routeLink'.
547592routeLinkDynAttr
548− :: forall t m a route.
593+ :: forall t m a route js.
549594 ( DomBuilder t m
550595 , PostBuild t m
551596 , RouteToUrl (R route) m
552597 , SetRoute t (R route) m
598+ , Prerender js t m
553599 )
554600 => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
555601 -> Dynamic t (R route) -- ^ Target route
556602 -> m a -- ^ Child widget of the @a@ element
557603 -> m a
558604routeLinkDynAttr dAttr dr w = do
605+ (e, a) <- routeLinkDynAttrImpl dAttr dr w
606+ scrollToTop e
607+ return a
608+
609+-- | Raw implementation of 'routeLinkDynAttr'. Does not scroll to the top of the page on clicks.
610+routeLinkDynAttrImpl
611+ :: forall t m a route.
612+ ( DomBuilder t m
613+ , PostBuild t m
614+ , RouteToUrl (R route) m
615+ , SetRoute t (R route) m
616+ )
617+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
618+ -> Dynamic t (R route) -- ^ Target route
619+ -> m a -- ^ Child widget of the @a@ element
620+ -> m (Event t (), a)
621+routeLinkDynAttrImpl dAttr dr w = do
559622 enc <- askRouteToUrl
560623 er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
561624 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
562625 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
563626 & elementConfig_modifyAttributes .~ er
564627 (e, a) <- element "a" cfg w
565− setRoute $ tag (current dr) $ domEvent Click e
566− return a
628+ let clk = domEvent Click e
629+ setRoute $ tag (current dr) clk
630+ return (clk, a)
567631
568632-- On ios due to sandboxing when loading the page from a file adapt the
569633-- path to be based on the hash.
lib/selftest/src/Obelisk/SelfTest.hs−12
@@ -27,7 +27,6 @@ import Shelly hiding (cp)
2727import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2828import System.Environment
2929import System.Exit (ExitCode (..))
30−import System.FilePath (replaceBaseName, takeBaseName)
3130import System.Which (staticWhich)
3231import qualified System.Info
3332import System.IO (Handle, hClose, hIsEOF, hGetContents)
@@ -217,9 +216,6 @@ main = do
217216 describe "ob run" $ {- NOT parallel $ -} do
218217 it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
219218 it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
220− it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
221− changeCabalPackageName "backend/backend.cabal" "new-backend"
222− testObRunInDir' Nothing httpManager
223219
224220 describe "obelisk project" $ parallel $ do
225221 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
@@ -426,14 +422,6 @@ obRunCheck httpManager _stdout frontendUri = do
426422 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
427423 "Request to frontend server failed: " <> T.pack (show r)
428424
429−-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
430−changeCabalPackageName :: FilePath -> Text -> Sh ()
431−changeCabalPackageName cabalFile newName = do
432− contents <- readfile cabalFile
433− writefile (replaceBaseName cabalFile (T.unpack newName)) $
434− T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
435− rm cabalFile
436−
437425getFreePorts :: Int -> IO [Socket.PortNumber]
438426getFreePorts 0 = pure []
439427getFreePorts n = Socket.withSocketsDo $ do