om-elm 2.0.1.2 → 2.1.0.0
raw patch · 4 files changed
+314/−91 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- System.Elm.Middleware: elmSite :: Map PathInfo FilePath -> Q (TExp Middleware)
+ System.Elm.Compile: Debug :: Mode
+ System.Elm.Compile: Dev :: Mode
+ System.Elm.Compile: Html :: ElmOutputFormat
+ System.Elm.Compile: JavaScript :: ElmOutputFormat
+ System.Elm.Compile: Optimize :: Mode
+ System.Elm.Compile: compileElm :: Mode -> ElmOutputFormat -> FilePath -> Q String
+ System.Elm.Compile: data ElmOutputFormat
+ System.Elm.Compile: data Mode
+ System.Elm.Compile: instance GHC.Internal.Classes.Eq System.Elm.Compile.ElmOutputFormat
+ System.Elm.Compile: instance GHC.Internal.Classes.Eq System.Elm.Compile.Mode
+ System.Elm.Compile: instance GHC.Internal.Show.Show System.Elm.Compile.ElmOutputFormat
+ System.Elm.Compile: instance GHC.Internal.Show.Show System.Elm.Compile.Mode
Files
- CHANGELOG.md +8/−0
- om-elm.cabal +5/−3
- src/System/Elm/Compile.hs +265/−0
- src/System/Elm/Middleware.hs +36/−88
+ CHANGELOG.md view
@@ -0,0 +1,8 @@+# Changelog++## 2.1.0.0 — 2026-07-07++- Added `System.Elm.Compile` with `compileElm` for embedding compiled+ Elm JavaScript or HTML in your Haskell program at build time.+- Removed the deprecated `elmSite` function; use `elmSiteDev`+ instead.
om-elm.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: om-elm-version: 2.0.1.2+version: 2.1.0.0 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@@ -18,7 +18,8 @@ copyright: 2025 Owens Murray, LLC. category: Web build-type: Simple-extra-source-files: README.md+extra-source-files: CHANGELOG.md+ README.md common warnings@@ -32,7 +33,8 @@ library import: warnings- exposed-modules: + exposed-modules:+ System.Elm.Compile System.Elm.Middleware -- other-modules: -- other-extensions:
+ src/System/Elm/Compile.hs view
@@ -0,0 +1,265 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++{- |+ Core compile-time utilities for embedding Elm programs in Haskell.++ This module runs the @elm@ compiler during Template Haskell+ splices and returns the compiled output as a 'String'. It is the+ low-level building block used by higher-level modules such as+ "System.Elm.Middleware".++ == Quick start++ 1) Ensure @elm@ is available when GHC runs Template Haskell. If+ you use Cabal, add 'System.Elm.Middleware.requireElm' to your+ @Setup.hs@ (see "System.Elm.Middleware" for details).++ 2) Call 'compileElm' from a Template Haskell splice:++ > import Language.Haskell.TH+ > import System.Elm.Compile+ >+ > embeddedJs :: Q String+ > embeddedJs = compileElm Dev JavaScript "frontend/Main.elm"++ 3) Splice the result into your program:++ > myJs :: String+ > myJs = $(embeddedJs)++ == Choosing a 'Mode'++ 'Mode' controls which flags are passed to @elm make@:++ * 'Dev' — no extra flags (the default for local development)+ * 'Debug' — passes @--debug@ (debug runtime, better errors)+ * 'Optimize' — passes @--optimize@ (smaller, faster output)++ Examples:++ > compileElm Dev JavaScript "frontend/Main.elm"+ >+ > compileElm Debug JavaScript "frontend/Main.elm"+ >+ > compileElm Optimize JavaScript "frontend/Main.elm"++ == Choosing an 'ElmOutputFormat'++ 'ElmOutputFormat' selects whether @elm make@ produces a standalone+ JavaScript bundle or a complete HTML page:++ * 'JavaScript' — compile to @.js@ (typical for SPAs loaded by your+ own HTML template)+ * 'Html' — compile to a self-contained @.html@ page++ Examples:++ > -- JavaScript bundle for a custom HTML shell+ > compileElm Dev JavaScript "frontend/Main.elm"+ >+ > -- Full HTML page served as-is+ > compileElm Dev Html "frontend/Main.elm"++ == Embedding in other types++ 'compileElm' returns a plain 'String'. Convert it however your+ application needs:++ > import qualified Data.Text as T+ >+ > embeddedText :: Q T.Text+ > embeddedText = T.pack <$> compileElm Dev JavaScript "frontend/Main.elm"++ > import qualified Data.ByteString.Char8 as BS8+ >+ > embeddedBytes :: Q BS8.ByteString+ > embeddedBytes = BS8.pack <$> compileElm Dev JavaScript "frontend/Main.elm"++ == Using with typed splices++ For a typed expression splice, bind the compiled content in @Q@ and+ lift it:++ > import Language.Haskell.TH+ >+ > myJs :: String+ > myJs = $(do+ > js <- compileElm Optimize JavaScript "frontend/Main.elm"+ > [| js |]+ > )++ == Recompilation++ 'compileElm' registers the @.elm@ source file as a Template Haskell+ dependency via 'addDependentFile'. GHC can re-run the splice when+ that file changes, but that only effectively works if the @.elm@+ file is also listed under @extra-source-files@ in your @.cabal@ file+ — otherwise Cabal may not rebuild the Haskell module when the Elm+ source changes.++ Example:++ > extra-source-files:+ > frontend/Main.elm++ == Relationship to "System.Elm.Middleware"++ If you want to serve compiled Elm as WAI 'Middleware', use+ "System.Elm.Middleware" instead — it wraps 'compileElm' for that+ purpose. Use this module when you want the compiled output+ directly (for example, embedding JavaScript in a HTML template you+ write yourself, or serving it through your own HTTP layer).++-}+module System.Elm.Compile (+ Mode (..),+ ElmOutputFormat (..),+ compileElm,+)+where++import Control.Exception.Safe (tryAny)+import Control.Monad (Monad((>>=)), void)+import Data.String (IsString)+import Language.Haskell.TH (Q, runIO)+import Language.Haskell.TH.Syntax (addDependentFile)+import Prelude+ ( Bool(True), Functor(fmap), Maybe(Just, Nothing), MonadFail(fail)+ , Semigroup((<>)), Show(show), ($), (++), (.), Eq, FilePath, String, putStrLn+ )+import System.Directory (createDirectory, removeDirectoryRecursive)+import System.Exit (ExitCode(ExitSuccess))+import System.Posix+ ( ProcessStatus(Exited), executeFile, forkProcess, getProcessStatus+ )+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8++data Mode+ = Optimize+ | Dev+ | Debug+ deriving stock (Eq, Show)+++data ElmOutputFormat+ = JavaScript+ | Html+ deriving stock (Eq, Show)+++buildDir :: (IsString a) => a+buildDir = ".om-elm-build-dir"+++{- |+ Compile an Elm program at Template Haskell time.++ This function invokes @elm make@, reads the compiler output from a+ temporary build directory, and returns the contents as a 'String'.+ The caller is responsible for interpreting the result — for example,+ choosing an HTTP content type or converting to 'Text'.++ == Parameters++ * __mode__ — compiler flags ('Dev', 'Debug', or 'Optimize')+ * __format__ — output shape ('JavaScript' or 'Html')+ * __elmFile__ — path to the @.elm@ entrypoint, relative to the+ project root (same path you would pass to @elm make@ on the command+ line)++ == Basic examples++ Compile a JavaScript bundle with default (dev) settings:++ > compileElm Dev JavaScript "frontend/Main.elm"++ Compile an optimized JavaScript bundle:++ > compileElm Optimize JavaScript "frontend/Main.elm"++ Compile a self-contained HTML page:++ > compileElm Dev Html "frontend/Main.elm"++ == Splicing into top-level definitions++ > myJs :: String+ > myJs = $(compileElm Dev JavaScript "frontend/Main.elm")++ > myHtml :: String+ > myHtml = $(compileElm Dev Html "frontend/Main.elm")++ == Splicing into a larger Template Haskell computation++ > import Language.Haskell.TH+ >+ > myJs :: String+ > myJs = $(do+ > js <- compileElm Optimize JavaScript "frontend/Main.elm"+ > [| js |]+ > )++ == Combining mode and format++ > -- Debug runtime, JavaScript output+ > compileElm Debug JavaScript "frontend/Main.elm"+ >+ > -- Optimized production build, HTML output+ > compileElm Optimize Html "frontend/Main.elm"+ >+ > -- Dev build, JavaScript output (most common for SPAs)+ > compileElm Dev JavaScript "frontend/Main.elm"++ == Requirements++ * The @elm@ executable must be on @PATH@ when GHC runs this splice.+ * The @elmFile@ path must exist at compile time.+ * GHC can re-run the splice when @elmFile@ changes (via+ 'addDependentFile'), but that only effectively works if @elmFile@+ is also listed under @extra-source-files@ in your @.cabal@ file+ (see the module documentation for an example).++ See the module documentation for setup instructions and more+ embedding examples.++-}+compileElm+ :: Mode+ -> ElmOutputFormat+ -> FilePath+ -> Q String+compileElm mode format elmFile = do+ addDependentFile elmFile+ runIO $ do+ void . tryAny $ removeDirectoryRecursive buildDir+ createDirectory buildDir+ putStrLn $ "Compiling elm file: " ++ elmFile+ let+ flags :: [String]+ flags =+ case mode of+ Debug -> ["--debug"]+ Dev -> []+ Optimize -> ["--optimize"]+ forkProcess+ (+ executeFile "elm" True ([+ "make",+ elmFile,+ "--output=" <> buildFile format+ ] ++ flags) Nothing+ ) >>= getProcessStatus True True >>= \case+ Nothing -> fail "elm should have ended."+ Just (Exited ExitSuccess) ->+ fmap BS8.unpack (BS.readFile (buildFile format))+ e -> fail $ "elm failed with: " ++ show e+++buildFile :: ElmOutputFormat -> FilePath+buildFile = \case+ JavaScript -> buildDir <> "/elm.js"+ Html -> buildDir <> "/elm.html"
src/System/Elm/Middleware.hs view
@@ -27,7 +27,7 @@ 2) Modify your @Setup.hs@ file, using 'requireElm'. - 3) Include a 'Middleware' template-haskell splice, using 'elmSite',+ 3) Include a 'Middleware' template-haskell splice, using 'elmSiteDev', in the appropriate place in your code. See the function documnetation for more details.@@ -35,7 +35,6 @@ -} module System.Elm.Middleware ( requireElm- , elmSite , elmSiteDebug , elmSiteDev , elmSiteOptimize@@ -43,9 +42,6 @@ ) where --import Control.Exception.Safe (tryAny)-import Control.Monad (void) import Data.Map (Map) import Data.String (IsString(fromString)) import Data.Text (Text)@@ -58,30 +54,23 @@ ( ConfigFlags(configVerbosity), fromFlagOrDefault ) import Distribution.Verbosity (normal)-import Language.Haskell.TH (Code(examineCode), Q, TExp, runIO)-import Language.Haskell.TH.Syntax (addDependentFile)+import Language.Haskell.TH (Code(examineCode), Q, TExp) import Network.HTTP.Types (methodNotAllowed405, ok200) import Network.Wai ( Request(pathInfo, requestMethod), Application, Middleware, responseLBS ) import Prelude- ( Bool(True), Eq((==)), Foldable(length), Functor(fmap), Maybe(Just, Nothing)- , Monad((>>=)), MonadFail(fail), Semigroup((<>)), Show(show)- , Traversable(mapM), ($), (++), (.), (<$>), (=<<), FilePath, String, putStrLn+ ( Applicative(pure), Bool(True), Eq((==)), Foldable(length), Functor(fmap)+ , Maybe(Just, Nothing), Traversable(mapM), ($), (++), (=<<), FilePath, String , reverse, take ) import Safe (lastMay)-import System.Directory (createDirectory, removeDirectoryRecursive)-import System.Exit (ExitCode(ExitSuccess))-import System.Posix- ( ProcessStatus(Exited), executeFile, forkProcess, getProcessStatus+import System.Elm.Compile+ ( ElmOutputFormat(Html, JavaScript), Mode(Debug, Dev, Optimize), compileElm )-import qualified Data.ByteString as BS-import qualified Data.ByteString.Char8 as BS8 import qualified Data.Map as Map import qualified Data.Text as T - {- | Add the elm program requirements to a set of build hooks. This is expected to be used in your Setup.hs file thusly:@@ -121,7 +110,7 @@ The typed template-haskell splice: - > $$(elmSite $ Map.fromList [+ > $$(elmSiteDev $ Map.fromList [ > (["app.js"], "elm/Your/Elm/Module/App.elm") > ]) @@ -136,12 +125,6 @@ elmSiteDev = elmSite2 Dev -{-| Deprecated. Synonym for 'elmSiteDev'. -}-{-# DEPRECATED elmSite "Use elmSiteDev instead. This function will be removed in a future release." #-}-elmSite :: Map PathInfo FilePath -> Q (TExp Middleware)-elmSite = elmSite2 Dev-- {- | Like 'elmSiteDev', but serve the debug elm runtime. -} elmSiteDebug :: Map PathInfo FilePath -> Q (TExp Middleware) elmSiteDebug = elmSite2 Debug@@ -154,11 +137,7 @@ elmSite2 :: Mode -> Map PathInfo FilePath -> Q (TExp Middleware) elmSite2 mode spec =- buildMiddleware =<<- mapM (\(u, c) -> (u,) <$> c) [- (uriPath, compileElm uriPath elmFile)- | (fmap T.unpack -> uriPath, elmFile) <- Map.toList spec- ]+ buildMiddleware =<< mapM compileResource (Map.toList spec) where {- | Construct the middleware from a set of compiled elm resources. -} buildMiddleware :: [([String], (String, String))] -> Q (TExp Middleware)@@ -167,17 +146,17 @@ [|| let apps = Map.fromList[- (uriPath, buildApp contentType content)- | (fmap T.pack -> uriPath, (contentType, content)) <- resources+ (uriPath, buildApp contentTypeHeader content)+ | (fmap T.pack -> uriPath, (contentTypeHeader, content)) <- resources ] {- | Build the application that serves a single elm resource. -} buildApp :: String -> String -> Application- buildApp contentType content req respond = respond $+ buildApp contentTypeHeader content req respond = respond $ case requestMethod req of "GET" -> responseLBS ok200- [("Content-Type", fromString contentType)]+ [("Content-Type", fromString contentTypeHeader)] (fromString content) _ -> responseLBS methodNotAllowed405 [("Allow", "GET")] "" in@@ -187,66 +166,35 @@ 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- let- flags :: [String]- flags =- case mode of- Debug -> ["--debug"]- Dev -> []- Optimize -> ["--optimize"]- forkProcess- (- executeFile "elm" True ([- "make",- elmFile,- "--output=" <> buildFile- ] ++ flags) Nothing- ) >>= getProcessStatus True True >>= \case- Nothing -> fail "elm should have ended."- Just (Exited ExitSuccess) ->- (contentType,)- . BS8.unpack- <$> BS.readFile buildFile- e -> fail $ "elm failed with: " ++ show e- where- {- |- The name of the build directory. We have to have a build- directory because elm won't output compile results to- stdout. It will only output them to files.- -}- buildDir :: (IsString a) => a- buildDir = ".om-elm-build-dir"+ compileResource+ :: (PathInfo, FilePath)+ -> Q ([String], (String, String))+ compileResource (uriPathText, elmFile) = do+ let+ uriPath :: [String]+ uriPath = fmap T.unpack uriPathText - {- | 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"+ format :: ElmOutputFormat+ format = outputFormat uriPath+ content <- compileElm mode format elmFile+ pure (uriPath, (contentType format, content)) - buildFile :: FilePath- buildFile = buildDir <> case lastMay uriPath of- Just (endsWith ".js" -> True) -> "/elm.js"- _ -> "/elm.html"+ outputFormat :: [String] -> ElmOutputFormat+ outputFormat uriPath =+ case lastMay uriPath of+ Just (endsWith ".js" -> True) -> JavaScript+ _ -> Html - endsWith :: String -> String -> Bool- endsWith ending str =- take (length ending) (reverse str) == reverse ending+ contentType :: ElmOutputFormat -> String+ contentType = \case+ JavaScript -> "text/javascript"+ Html -> "text/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]---data Mode- = Optimize- | Dev- | Debug-