packages feed

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 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-