om-elm-2.1.0.0: src/System/Elm/Compile.hs
{-# 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"