{-# language OverloadedStrings #-} {-# language TemplateHaskell #-} -- | Snaplet that serves javascript files compiled with ghcjs -- (). 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"