om-elm (empty) → 1.0.0.1
raw patch · 5 files changed
+288/−0 lines, 5 filesdep +Cabaldep +basedep +bytestringsetup-changed
Dependencies added: Cabal, base, bytestring, containers, directory, http-types, safe, safe-exceptions, template-haskell, text, unix, wai
Files
- LICENSE +20/−0
- README.md +10/−0
- Setup.hs +2/−0
- om-elm.cabal +46/−0
- src/System/Elm/Middleware.hs +210/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2018 Owens Murray, LLC.++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.
+ README.md view
@@ -0,0 +1,10 @@+# om-elm++This package provides utilities for serving Elm programs directly+from your Haskell binary. It uses TemplateHaskell to compile your Elm+program at build time, and construct a WAI Middleware which intercepts+requests appropriate to the Elm program, and passing other requests to+a downstream WAI Application. It is useful for bundling the browser side+of a web application with its backing web services implementation.++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ om-elm.cabal view
@@ -0,0 +1,46 @@+-- Initial om-elm.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/++name: om-elm+version: 1.0.0.1+synopsis: Haskell utilities for building embedded Elm programs.+description: This package provides utilities for serving Elm programs+ directly from your Haskell binary. It uses TemplateHaskell+ to compile your Elm program at build time, and construct a+ WAI Middleware which intercepts requests appropriate to+ the Elm program, and passing other requests to a+ downstream WAI Application. It is useful for bundling the+ browser side of a web application with its backing web+ services implementation.+homepage: https://github.com/owensmurray/om-elm+license: MIT+license-file: LICENSE+author: Rick Owens+maintainer: rick@owensmurray.com+copyright: 2018 Owens Murray, LLC.+category: Web+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ exposed-modules: + System.Elm.Middleware+ -- other-modules: + -- other-extensions: + build-depends:+ Cabal >= 2.0.1.1 && < 2.1,+ base >= 4.9 && < 4.11,+ bytestring >= 0.10.8.1 && < 0.11,+ containers >= 0.5.7.1 && < 0.6,+ directory >= 1.3.0.0 && < 1.4,+ http-types >= 0.9.1 && < 0.10,+ safe >= 0.3.15 && < 0.4,+ safe-exceptions >= 0.1.6.0 && < 0.2,+ template-haskell >= 2.11.1.0 && < 2.13,+ text >= 1.2.2.2 && < 1.3,+ unix >= 2.7.2.1 && < 2.8,+ wai >= 3.2.1.1 && < 3.3+ hs-source-dirs: src+ default-language: Haskell2010+
+ src/System/Elm/Middleware.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ViewPatterns #-}++{- |+ This module contains utilities for compiling and bundling Elm programs+ directly into your Haskell executable binary. It is useful for the+ case where you want to bundle a front-end Elm web app along with the+ backing services that support it, and especially for when the two+ components are part of the same codebase. It produces WAI Middleware,+ and is thus compatible with a wide range server-side frameworks.++ Usage is designed to be as simple as possible. There are 3 steps:++ 1) Change your .cabal file to use a \"Custom\" build type, and add+ the appropriate custom build dependencies.++ > build-type: Custom+ > ...+ > custom-setup+ > setup-depends:+ > Cabal,+ > base,+ > om-elm++ 2) Modify your @Setup.hs@ file, using 'requireElm'.++ 3) Include a 'Middleware' template-haskell splice, using 'elmSite',+ in the appropriate place in your code.++ See the function documnetation for more details.++-}+module System.Elm.Middleware (+ requireElm,+ elmSite,+ elmSiteDebug,+ PathInfo,+) where+++import Control.Exception.Safe (tryAny)+import Control.Monad (void)+import Data.Bool (bool)+import Data.Map (Map)+import Data.Monoid ((<>))+import Data.String (IsString, fromString)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8)+import Distribution.Simple (hookedPrograms, simpleUserHooks, preConf,+ UserHooks)+import Distribution.Simple.Program (simpleProgram, Program,+ configureAllKnownPrograms, requireProgram, defaultProgramDb)+import Distribution.Simple.Setup (fromFlagOrDefault, configVerbosity)+import Distribution.Verbosity (normal)+import Language.Haskell.TH (Q, TExp, runIO)+import Language.Haskell.TH.Syntax (addDependentFile)+import Network.HTTP.Types (ok200, methodNotAllowed405)+import Network.Wai (Middleware, Application, pathInfo, requestMethod,+ responseLBS)+import Safe (lastMay)+import System.Directory (removeDirectoryRecursive, createDirectory)+import System.Exit (ExitCode(ExitSuccess))+import System.Posix (ProcessStatus(Exited), forkProcess, executeFile,+ getProcessStatus)+import qualified Data.ByteString as BS+import qualified Data.Map as Map+import qualified Data.Text as T+++{- |+ Add the elm-make program requirements to a set of build hooks. This is+ expected to be used in your Setup.hs file thusly:++ > import Distribution.Simple (defaultMainWithHooks, simpleUserHooks)+ > import System.Elm.Middleware (requireElm)+ > + > main = defaultMainWithHooks (requireElm simpleUserHooks)++-}+requireElm :: UserHooks -> UserHooks+requireElm hooks =+ hooks {+ hookedPrograms = hookedPrograms hooks ++ [elmProg],++ preConf = \args flags -> do+ let verbosity = fromFlagOrDefault normal (configVerbosity flags)+ db <- configureAllKnownPrograms verbosity defaultProgramDb+ _ <- requireProgram verbosity elmProg db+ preConf simpleUserHooks args flags+ }+ where+ {- | A description of the elm-make program. -}+ elmProg :: Program+ elmProg = simpleProgram "elm-make"+++{- |+ Template Haskell method to create a 'Middleware' that serves a set of+ elm programs. The elm programs are compiled into HTML at compile time,+ and that HTML is included directly in your executable.++ The parameter is a map of 'pathInfo's to elm program module file. The+ elm program located at the file is served whenever the pathInfo matches+ that of the request. Any non-matching request is forwarded to the+ downstream 'Application'.++ The typed template-haskell splice:++ > $$(elmSite $ Map.fromList [+ > (["app.js"], "elm/Your/Elm/Module/App.elm")+ > ])++ will construct a WAI 'Middleware' which serves the compiled elm program on+ @/app.js@.++-}+elmSite :: Map PathInfo FilePath -> Q (TExp Middleware)+elmSite = elmSite2 False+++{- | Like 'elmSite', but serve the debug elm runtime. -}+elmSiteDebug :: Map PathInfo FilePath -> Q (TExp Middleware)+elmSiteDebug = elmSite2 True+++elmSite2 :: Bool -> Map PathInfo FilePath -> Q (TExp Middleware)+elmSite2 debug spec =+ buildMiddleware =<< (+ mapM (\(u, c) -> (u,) <$> c) [+ (uriPath, compileElm uriPath elmFile)+ | (fmap T.unpack -> uriPath, elmFile) <- Map.toList spec+ ]+ )+ where+ {- | Construct the middleware from a set of compiled elm resources. -}+ buildMiddleware :: [([String], (String, String))] -> Q (TExp Middleware)+ buildMiddleware resources = [||+ let+ apps = Map.fromList[+ (uriPath, buildApp contentType content)+ | (fmap T.pack -> uriPath, (contentType, content)) <- resources+ ]+ {- | Build the application that serves a single elm resource. -}+ buildApp :: String -> String -> Application+ buildApp contentType content req respond = respond $+ case requestMethod req of+ "GET" ->+ responseLBS+ ok200+ [("Content-Type", fromString contentType)]+ (fromString content)+ _ -> responseLBS methodNotAllowed405 [("Allow", "GET")] ""+ in+ \downstream req respond ->+ case Map.lookup (pathInfo req) apps of+ Nothing -> downstream req respond+ Just app -> app req respond+ ||]++ compileElm :: [String] -> FilePath -> Q (String, String)+ compileElm uriPath elmFile = do+ addDependentFile elmFile+ runIO $ do+ void . tryAny $ removeDirectoryRecursive buildDir+ createDirectory buildDir+ putStrLn $ "Compiling elm file: " ++ elmFile+ forkProcess (executeFile "elm-make" True ([+ elmFile,+ "--yes",+ "--output=" <> buildFile+ ] ++ bool [] ["--debug"] debug) Nothing) >>= getProcessStatus True True >>= \case+ Nothing -> fail "elm-make should have ended."+ Just (Exited ExitSuccess) ->+ (contentType,)+ . T.unpack+ . decodeUtf8+ <$> BS.readFile buildFile+ e -> fail $ "elm-make failed with: " ++ show e+ where+ {- |+ The name of the build directory. We have to have a build+ directory because elm-make won't output compile results to+ stdout. It will only output them to files.+ -}+ buildDir :: (IsString a) => a+ buildDir = ".om-elm-build-dir"++ {- | Figure out if we are compiling to javascript or html. -}+ contentType :: String+ contentType = case lastMay uriPath of+ Just (endsWith ".js" -> True) -> "text/javascript"+ _ -> "text/html"++ buildFile :: FilePath+ buildFile = buildDir <> case lastMay uriPath of+ Just (endsWith ".js" -> True) -> "/elm.js"+ _ -> "/elm.html"++ endsWith :: String -> String -> Bool+ endsWith ending str =+ take (length ending) (reverse str) == reverse ending+++{- | A WAI uri path, as per the meaning of 'pathInfo'. -}+type PathInfo = [Text]++