{-# LANGUAGE OverloadedStrings #-} {-| This module provides a few functions for conveniently serving Elm files through the Snap web framework. Any changes made to the served files will be reflected in the browser upon a refresh. The easiest way to get started is to use the default ElmOptions: > app = makeSnaplet ... $ do > opts <- defaultElmOptions > ... > addRoutes $ routes opts > ... Then, provide routes to the Elm runtime, and to any Elm files you wish to serve. > routes opts = > [ ("/elm", serveElm opts "static/elm/test.elm") > , ... > , serveElmRuntime opts > ] Additionally, you can customize the URI of the Elm runtime, the file path to the Elm runtime, or the paths to the directores that Elm will use to build and cache the compiled files. > app = makeSnaplet ... $ do > opts <- setElmBuildPath "/tmp/elm" <$> > setElmVerbose True <$> > defaultElmOptions > ... > addRoutes $ routes opts > ... The 'FilePath's supplied to setElm{Source,Build,Cache}Path can be relative or absolute. -} module Snap.Elm where import Control.Monad import Control.Monad.IO.Class import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C8 import Data.Monoid import qualified Data.Text as T import Snap.Core import Snap.Util.FileServe import qualified Language.Elm as Elm import GHC.IO.Handle import System.Directory import System.Exit import System.FilePath import System.Process -- | A set of options to coordinate the serving of Elm files and runtime. data ElmOptions = ElmOptions { elmIsVerbose :: Bool , elmRuntimeURI :: ByteString , elmRuntimePath :: FilePath , elmSourcePath :: FilePath , elmBuildPath :: FilePath , elmCachePath :: FilePath } -- | The default set of options for serving Elm files. -- This will use "static/js/elm-runtime.js" as the URI -- for the Elm runtime, so you should use a custom route -- if the route conflicts with another, for some reason. defaultElmOptions :: MonadIO m => m ElmOptions defaultElmOptions = do let v = False let uri = "/static/js/elm-runtime.js" rt <- liftIO Elm.runtime let s = "." let b = "elm-build" let c = "elm-cache" return $ ElmOptions v uri rt s b c -- | Tell Elm to be verbose (print all executed commands and their output -- to stdout), or quiet (print nothing). setElmVerbose :: Bool -> ElmOptions -> ElmOptions setElmVerbose v opts = opts { elmIsVerbose = v } -- | Set the URI at which to serve the Elm runtime JS file. setElmRuntimeURI :: ByteString -> ElmOptions -> ElmOptions setElmRuntimeURI uri opts = opts { elmRuntimeURI = uri } -- | Set the 'FilePath' to some custom Elm runtime. setElmRuntimePath :: FilePath -> ElmOptions -> ElmOptions setElmRuntimePath rt opts = opts { elmRuntimePath = rt } -- | Set the directory to look for .elm files. -- This allows for the @elm@ binary to properly find -- local Elm modules. setElmSourcePath :: FilePath -> ElmOptions -> ElmOptions setElmSourcePath src opts = opts { elmSourcePath = src } -- | Set the directory to use for storing the compiled .html -- that @elm@ produces. setElmBuildPath :: FilePath -> ElmOptions -> ElmOptions setElmBuildPath bld opts = opts { elmBuildPath = bld } -- | Set the directory to use for storing the .elmi and .elmo -- files that @elm@ produces. setElmCachePath :: FilePath -> ElmOptions -> ElmOptions setElmCachePath cch opts = opts { elmCachePath = cch } -- | Compile and serve an Elm file. serveElm :: MonadSnap m => ElmOptions -> FilePath -> m () serveElm opts fp = when (takeExtension fp == ".elm") $ do cd <- liftIO getCurrentDirectory let runtimeURI = C8.unpack (elmRuntimeURI opts) let sourcePath = makeAbsolutePath (elmSourcePath opts) cd let buildPath = makeAbsolutePath (elmBuildPath opts) cd let cachePath = makeAbsolutePath (elmCachePath opts) cd let args = [ "--make" , "--runtime=" ++ runtimeURI , "--build-dir=" ++ buildPath , "--cache-dir=" ++ cachePath , fp ] ifVerbose $ liftIO $ do putStrLn "Elm:" putStrLn $ " $ cd " ++ elmSourcePath opts putStrLn $ unwords $ " $ elm" : args (_,hOut,hErr,pid) <- liftIO $ runInteractiveProcess "elm" args (Just sourcePath) Nothing out <- liftIO $ hGetContents hOut err <- liftIO $ hGetContents hErr ec <- liftIO $ waitForProcess pid ifVerbose $ liftIO $ putStrLn $ indent out case ec of ExitFailure _ -> writeText $ T.unlines [ "Failed to build Elm file (" <> T.pack fp <> "):" , " " <> T.pack err ] ExitSuccess -> serveFile $ buildPath replaceExtension fp "html" where ifVerbose = when $ elmIsVerbose opts indent = unlines . map (" " ++) . lines makeAbsolutePath :: FilePath -> FilePath -> FilePath makeAbsolutePath p cd = case p of "" -> cd "." -> cd '/':_ -> p _ -> cd p -- | Serve a directory of Elm files, using a route parameter. -- -- For example, a list of routes could contain: -- -- > routes opts = -- > [ ... -- > , ("elm/:file", jerveElmDirectory opts "file") -- > ] -- -- Then, following the example, if the ElmOptions contained @static/elm@ -- as the sourcePath, the route @elm/file.elm@ would -- be handled by @serveElm "file.elm"@, run with the working directory -- @static/elm@. serveElmDirectory :: MonadSnap m => ElmOptions -> ByteString -- ^ Route parameter name by which to identify files -> m () serveElmDirectory opts pm = do mf <- getParam pm whenJust mf (serveElm opts . C8.unpack) where whenJust :: Monad m => Maybe a -> (a -> m ()) -> m () whenJust m f = maybe (return ()) f m serveElmDirectory' :: MonadSnap m => ElmOptions -> ByteString -> (ByteString, m ()) serveElmDirectory' opts d = (uri,handler) where param = "file" uri | C8.null d = "/:" <> param | C8.last d == '/' = d <> ":" <> param | otherwise = d <> "/:" <> param handler = do mf <- getParam param whenJust mf (serveElm opts . C8.unpack) whenJust :: Monad m => Maybe a -> (a -> m ()) -> m () whenJust m f = maybe (return ()) f m -- | A route handler for the Elm runtime. If given the 'ElmOptions' used -- by 'serveElm', it will place the runtime at the route the Elm file -- will expect, as per the @