{-# LANGUAGE OverloadedStrings #-} {-| A Haskell library wrapper around the Elm executable, to build files from within Haskell. For more information on Elm, see http://elm-lang.org. There are two main steps to using this library: converting Elm source to a Module structure, then compiling various modules. To compile a string to a module, simply do > let auxModule = moduleFromString (pack "Aux") (pack $ "module Aux where\n" ++ "x = 3") or > let mainModule = moduleFromString (pack "Main") (pack $ "import Aux\n" ++ "main = plainText (show Aux.x)") Note that the first argument must match the name given in the @module X where@ declaration in your elm file. Both arguments must be Text, not String. You can use `moduleFromFile` similarly. Once you have some modules, you can compile them into JavaScript or HTML: > Right js <- buildModulesWithOptions defaultOptions mainModule [auxModule] The first argument is always the module containing the @main@ definition for Elm. The list is the list of all files which are dependencies of the main module. Files are written to a temp directory, then compiled using the @--make@ option. A current limitation is that only single-directory structures are supported. -} module Language.Elm.Build ( Module, Javascript, BuildOptions(..), ModuleName, ModuleSource, defaultOptions, moduleFromString, moduleFromFile, buildModules, buildModulesWithOptions ) where import System.Process (readProcessWithExitCode) import System.Directory (doesFileExist) import System.Exit (ExitCode(..)) import Data.Maybe (catMaybes, fromMaybe) import System.IO.Temp (withTempDirectory) import Data.Text import qualified Data.Text.IO as TextIO -- | Synonym for module names (i.e. Data.Text, Main, etc.) type ModuleName = Text -- | Type for module source code type ModuleSource = Text -- | Wrapper for modules, which have a name and source code newtype InternalModule = Module (ModuleName, ModuleSource) -- | Opaque type representing an Elm module loaded from a string or file type Module = InternalModule -- | Type representing Javascript output (as a string) type Javascript = Text -- | Abstraction for the options given to the elm executable -- Note that not all Elm options may be avaliable data BuildOptions = BuildOptions { elmBinPath :: Maybe String, elmRuntimePath :: Maybe String, makeHtml :: Bool } -- |Default options are: `elm` as binary, no runtime given, and generate JS only defaultOptions :: BuildOptions defaultOptions = BuildOptions { elmBinPath = Nothing, elmRuntimePath = Nothing, makeHtml = False } -- | Generate a module with the given Module name (e.g. 'MyLib.Foo') -- and the given source code moduleFromString :: ModuleName -> ModuleSource -> Module moduleFromString name source = Module (name, source) -- | Read a module from a file, with the given module name and file path moduleFromFile :: ModuleName -> FilePath -> IO Module moduleFromFile name path = do src <- TextIO.readFile path return $ moduleFromString name src -- | Build a group of elm modules with the `elm` from the system `$PATH` -- generating JavaScript using the default runtime location buildModules :: Module -> [Module] -> IO (Either String Javascript) buildModules = buildModulesWithOptions defaultOptions -- | Given an elm "main" module, and a list of other modules, -- compile them using the `--make` option and the given options buildModulesWithOptions :: BuildOptions -> Module -> [Module] -> IO (Either String Javascript) buildModulesWithOptions options mainModule@(Module (mainName, _)) otherModules = withTempDirectory "" ".elm_temp" (\dir -> do mapM_ (writeElmSource dir) otherModules writeElmSource dir mainModule let binPath = fromMaybe "elm" $ elmBinPath options let runtimeOption = maybe Nothing (\path -> Just $ "--runtime=" ++ path) $ elmRuntimePath options let genJSOption = if (makeHtml options) then Nothing else (Just "--only-js") let resultExt = if (makeHtml options) then ".html" else ".js" let cmdlineOptions = ["--make", "--build-dir=" ++ dir ++ "/build", "--cache-dir=" ++ dir ++"/cache", "--src-dir=" ++ dir] ++ catMaybes [runtimeOption, genJSOption] ++ [ unpack mainName ++ ".elm"] (exitCode, stdout, stderr) <- readProcessWithExitCode binPath cmdlineOptions [] case exitCode of ExitFailure i -> return $ Left $ "Elm failed with exit code " ++ (show i) ++ " and errors:" ++ stdout ++ stderr _ -> do let outputPath = dir ++ "/build/" ++ unpack mainName ++ resultExt exists <- doesFileExist outputPath case exists of False -> return $ Left "Could not find output file from Elm" _ -> do retJS <- TextIO.readFile outputPath return $ Right retJS ) where writeElmSource dir (Module (moduleName, source)) = do let path = dir ++ "/" ++ unpack moduleName ++ ".elm" TextIO.writeFile path source