snaplet-ghcjs-0.1.0.0: Snap/Snaplet/GHCJS.hs
{-# language OverloadedStrings #-}
{-# language TemplateHaskell #-}
-- | Snaplet that serves javascript files compiled with ghcjs
-- (<https://github.com/valderman/ghcjs-compiler>). This Snaplet is meant to be
-- used for development. You can work on client side Haskell code and
-- immedietely test the code with a simple browser reload. It certainly adds
-- some overhead and is not meant to be used in production websites.
--
-- Usage:
--
-- Put haskell source files in the snaplet path (e.g. @$ROOT\/snaplets\/ghcjs@).
-- For every such haskell file there will be a new javascript file available via
-- http.
--
-- * Other files won't be served through http. The snaplet will 'mzero' on .hs,
-- .hi, .o and all other files.
--
-- * If any haskell file in the snaplet path is newer than the
-- requested javascript file, it will be recompiled. The ghcjs
-- snaplet does not track haskell import dependencies: recompilation
-- happens whether the js file is requested or not.
--
-- * If ghcjsc exits with an error code this snaplet will serve a special
-- javascript file that contains the error message as a comment and a
-- javascript command that will raise the error message as a javascript
-- exception.
--
-- Please add such a handler to your routes:
--
-- [ ...
-- , ("" , with ghcjs ghcjsServe)
-- ]
module Snap.Snaplet.GHCJS (
GHCJS,
snapletArgs,
initialize,
ghcjsServe,
) where
import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.State.Class as State
import Control.Lens (makeLenses)
import Data.List
import Data.String.Conversions
import Snap.Core
import Snap.Snaplet
import Snap.Util.FileServe
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.Process
import Text.Printf
-- | Internal data type for the ghcjs snaplet.
data GHCJS = GHCJS { _ghcjsc :: FilePath
, _snapletArgs :: [String]
}
makeLenses ''GHCJS
-- | Initializes the ghcjs snaplet. Use it with e.g. 'nestSnaplet'.
initialize :: [String] -> SnapletInit app GHCJS
initialize args = makeSnaplet "ghcjs" description Nothing $ do
cabalPackageDBs <- liftIO parseCabalPackageDBs
let packageArgs = cabalPackageDBs >>= (\a -> ["-package-db", a])
liftIO $ mapM_ putStrLn packageArgs
return $ GHCJS "ghcjs" (packageArgs ++ args)
where
description = "handler for delivering javascript files compiled with ghcjs"
-- The following is a hack. There is code to parse the cabal config properly
-- to find the sandbox, but it is in cabal-install which doesn't export it
-- There may also be a few ways of doing this with cabal, but they would
-- be even worse than the following:
parseCabalPackageDBs :: IO [String]
parseCabalPackageDBs = do
-- lines not indented are paths
-- TODO - validate properly
let isPath s = take 2 s /= (" "::String) && take 1 s == ("/" :: String)
dir <- getCurrentDirectory
writeFile "cabal.snaplet-ghcjs.config" $
unlines [ "-- Automatically generated by snaplet-ghcjs to help determine location of package db, working around https://github.com/haskell/cabal/pull/2859"
, "compiler: ghcjs"
, "-- Thankyou."
, ""]
(_stdin, Just stdoutH, Just stderrH, processHandle) <-
createProcess (proc "cabal" ["--config-file=cabal.snaplet-ghcjs.config", "exec", "ghcjs-pkg", "list"]){
cwd = Just dir,
std_out = CreatePipe,
std_err = CreatePipe
}
exitCode <- waitForProcess processHandle
stdout <- hGetContents stdoutH
stderr <- hGetContents stderrH
case exitCode of
-- Delete trailing colon
ExitSuccess -> return $ map init $ filter isPath $ lines stdout
ExitFailure _ -> do
putStrLn "snaplet-ghcjs WARNING: dirty hack failed. Please set -package-db yourself."
return []
ghcjsServe :: Handler app GHCJS ()
ghcjsServe = do
jsPath <- cs <$> rqPathInfo <$> getRequest
ghcjsDir <- getSnapletFilePath
if takeExtension jsPath /= ".js" then
mzero
else
deliverJS (dropExtension (ghcjsDir </> jsPath))
deliverJS :: FilePath -> Handler app GHCJS ()
deliverJS basename = do
hsExists <- liftIO $ doesFileExist (basename <.> "hs")
unless hsExists mzero
let jsFile = jsFileName basename
snapletDir <- getSnapletFilePath
jsNewer <- liftIO $ isJSNewer jsFile snapletDir
if jsNewer then
serveFile jsFile
else
compile basename
-- | Returns whether the given javascript file exists and is newer than
-- all Haskell files in the given directory.
isJSNewer :: FilePath -> FilePath -> IO Bool
isJSNewer jsFile dir = do
exists <- liftIO $ doesFileExist jsFile
if not exists then
return False
else do
hsFiles <- collectAllHsFiles dir
hsTimeStamps <- mapM getModificationTime hsFiles
jsTimeStamp <- getModificationTime jsFile
return (jsTimeStamp > maximum hsTimeStamps)
where
collectAllHsFiles :: FilePath -> IO [FilePath]
collectAllHsFiles dir = do
paths <- fmap (dir </>) <$>
filter (not . ("." `isPrefixOf`)) <$>
getDirectoryContents dir
(files, dirs) <- partitionM doesFileExist paths
let hsFiles = filter (\ f -> takeExtension f == ".hs") files
subHsFiles <- concat <$> mapM collectAllHsFiles dirs
return (hsFiles ++ subHsFiles)
partitionM :: Monad m => (a -> m Bool) -> [a] -> m ([a], [a])
partitionM pred (a : r) = do
is <- pred a
(yes, no) <- partitionM pred r
return $ if is then (a : yes, no) else (yes, a : no)
partitionM pred [] = return ([], [])
-- | Recompiles the file and serves it in case of success.
compile :: FilePath -> Handler app GHCJS ()
compile name = do
GHCJS
ghcjsc
snapletArgs <- State.get
let args = snapletArgs ++ [name <.> "hs"]
outfile = jsFileName name
dir <- getSnapletFilePath
(exitCode, message) <- liftIO $ do
(_stdin, Just stdoutH, Just stderrH, processHandle) <-
createProcess (proc ghcjsc args){
cwd = Just dir,
std_out = CreatePipe,
std_err = CreatePipe
}
exitCode <- waitForProcess processHandle
stdout <- hGetContents stdoutH
stderr <- hGetContents stderrH
return (exitCode, "\nGHCJS error:\n============\n" ++ stdout ++ stderr)
case exitCode of
ExitFailure _ ->
writeBS $ cs (printf ("/*\n\n%s\n\n*/\n\nthrow %s;") message (show message) :: String)
ExitSuccess -> serveFile outfile
-- At time of writing, there's no way to include js from packages.
-- Just throw everything up onto the frontend without worrying about
-- duplicate code etc.
jsFileName :: String -> FilePath
jsFileName name = name <.> "jsexe" </> "all.js"