11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DoAndIfThenElse #-}
3+ {-# LANGUAGE GADTs #-}
34{-# LANGUAGE GeneralizedNewtypeDeriving #-}
45{-# LANGUAGE MultiParamTypeClasses #-}
6+ {-# LANGUAGE OverloadedStrings #-}
7+ {-# LANGUAGE PatternGuards #-}
58{-# LANGUAGE RecordWildCards #-}
69{-# LANGUAGE ScopedTypeVariables #-}
10+ {-# LANGUAGE TemplateHaskell #-}
711{-# LANGUAGE TupleSections #-}
812
913module Main (main ) where
1014
1115import Prelude ()
1216import Prelude.Compat
1317
18+ import Data.FileEmbed (embedStringFile )
1419import Data.Monoid ((<>) )
20+ import Data.String (IsString (.. ))
21+ import Data.Text (Text , unpack )
22+ import Data.Traversable (for )
1523import Data.Version (showVersion )
1624
17- import Control.Applicative (many )
25+ import Control.Applicative (many , (<|>) )
26+ import Control.Concurrent (forkIO )
27+ import Control.Concurrent.MVar (MVar , newEmptyMVar , putMVar , takeMVar ,
28+ tryPutMVar )
29+ import Control.Concurrent.STM (TVar , atomically , newTVarIO , writeTVar ,
30+ readTVarIO ,
31+ TChan , newBroadcastTChanIO , dupTChan ,
32+ readTChan , writeTChan )
33+ import Control.Exception (fromException )
1834import Control.Monad
1935import Control.Monad.IO.Class (liftIO )
2036import Control.Monad.Trans.Class
@@ -23,21 +39,33 @@ import Control.Monad.Trans.State.Strict (StateT, evalStateT)
2339import Control.Monad.Trans.Reader (ReaderT , runReaderT )
2440
2541import qualified Language.PureScript as P
42+ import qualified Language.PureScript.Bundle as Bundle
2643import Language.PureScript.Interactive
2744
45+ import Network.HTTP.Types.Header (hContentType , hCacheControl ,
46+ hPragma , hExpires )
47+ import Network.HTTP.Types.Status (status200 , status404 , status503 )
48+ import qualified Network.Wai as Wai
49+ import qualified Network.Wai.Handler.Warp as Warp
50+ import qualified Network.Wai.Handler.WebSockets as WS
51+ import qualified Network.WebSockets as WS
52+
2853import qualified Options.Applicative as Opts
2954
3055import qualified Paths_purescript as Paths
3156
3257import System.Console.Haskeline
58+ import System.IO.UTF8 (readUTF8File )
3359import System.Exit
60+ import System.FilePath ((</>) )
3461import System.FilePath.Glob (glob )
62+ import System.Process (readProcessWithExitCode )
3563
3664-- | Command line options
3765data PSCiOptions = PSCiOptions
3866 { psciMultiLineMode :: Bool
3967 , psciInputFile :: [FilePath ]
40- , psciInputNodeFlags :: [ String ]
68+ , psciBackend :: Backend
4169 }
4270
4371multiLineMode :: Opts. Parser Bool
@@ -60,10 +88,21 @@ nodeFlagsFlag = Opts.option parser $
6088 where
6189 parser = words <$> Opts. str
6290
91+ port :: Opts. Parser Int
92+ port = Opts. option Opts. auto $
93+ Opts. long " port"
94+ <> Opts. short ' p'
95+ <> Opts. help " The web server port"
96+
97+ backend :: Opts. Parser Backend
98+ backend =
99+ (browserBackend <$> port)
100+ <|> (nodeBackend <$> nodeFlagsFlag)
101+
63102psciOptions :: Opts. Parser PSCiOptions
64103psciOptions = PSCiOptions <$> multiLineMode
65104 <*> many inputFile
66- <*> nodeFlagsFlag
105+ <*> backend
67106
68107version :: Opts. Parser (a -> a )
69108version = Opts. abortOption (Opts. InfoMsg (showVersion Paths. version)) $
@@ -92,6 +131,195 @@ getCommand singleLineMode = handleInterrupt (return (Right Nothing)) $ do
92131 go :: [String ] -> InputT m String
93132 go ls = maybe (return . unlines $ reverse ls) (go . (: ls)) =<< getInputLine " "
94133
134+ -- | Make a JavaScript bundle for the browser.
135+ bundle :: IO (Either Bundle. ErrorMessage String )
136+ bundle = runExceptT $ do
137+ inputFiles <- liftIO (glob (" .psci_modules" </> " node_modules" </> " *" </> " *.js" ))
138+ input <- for inputFiles $ \ filename -> do
139+ js <- liftIO (readUTF8File filename)
140+ mid <- Bundle. guessModuleIdentifier filename
141+ length js `seq` return (mid, js)
142+ Bundle. bundle input [] Nothing " PSCI"
143+
144+ indexJS :: IsString string => string
145+ indexJS = $ (embedStringFile " psci/static/index.js" )
146+
147+ indexPage :: IsString string => string
148+ indexPage = $ (embedStringFile " psci/static/index.html" )
149+
150+ -- | All of the functions required to implement a PSCi backend
151+ data Backend = forall state . Backend
152+ { _backendSetup :: IO state
153+ -- ^ Initialize, and call the continuation when the backend is ready
154+ , _backendEval :: state -> String -> IO ()
155+ -- ^ Evaluate JavaScript code
156+ , _backendReload :: state -> IO ()
157+ -- ^ Reload the compiled code
158+ , _backendShutdown :: state -> IO ()
159+ -- ^ Shut down the backend
160+ }
161+
162+ -- | Commands which can be sent to the browser
163+ data BrowserCommand
164+ = Eval (MVar String )
165+ -- ^ Evaluate the latest JS
166+ | Reload
167+ -- ^ Reload the page
168+
169+ -- | State for the browser backend
170+ data BrowserState = BrowserState
171+ { browserCommands :: TChan BrowserCommand
172+ -- ^ A channel which receives data when the compiled JS has
173+ -- been updated
174+ , browserShutdownNotice :: MVar ()
175+ -- ^ An MVar which becomes full when the server should shut down
176+ , browserIndexJS :: TVar (Maybe String )
177+ -- ^ A TVar holding the latest compiled JS
178+ , browserBundleJS :: TVar (Maybe String )
179+ -- ^ A TVar holding the latest bundled JS
180+ }
181+
182+ browserBackend :: Int -> Backend
183+ browserBackend serverPort = Backend setup evaluate reload shutdown
184+ where
185+ setup :: IO BrowserState
186+ setup = do
187+ shutdownVar <- newEmptyMVar
188+ cmdChan <- newBroadcastTChanIO
189+ indexJs <- newTVarIO Nothing
190+ bundleJs <- newTVarIO Nothing
191+
192+ let
193+ handleWebsocket :: WS. PendingConnection -> IO ()
194+ handleWebsocket pending = do
195+ conn <- WS. acceptRequest pending
196+ -- Fork a thread to keep the connection alive
197+ WS. forkPingThread conn 10
198+ -- Clone the command channel
199+ cmdChanCopy <- atomically $ dupTChan cmdChan
200+ -- Listen for commands
201+ forever $ do
202+ cmd <- atomically $ readTChan cmdChanCopy
203+ case cmd of
204+ Eval resultVar -> void $ do
205+ WS. sendTextData conn (" eval" :: Text )
206+ result <- WS. receiveData conn
207+ -- With many connected clients, all but one of
208+ -- these attempts will fail.
209+ tryPutMVar resultVar (unpack result)
210+ Reload -> do
211+ WS. sendTextData conn (" reload" :: Text )
212+
213+ shutdownHandler :: IO () -> IO ()
214+ shutdownHandler stopServer = void . forkIO $ do
215+ () <- takeMVar shutdownVar
216+ stopServer
217+
218+ onException :: Maybe Wai. Request -> SomeException -> IO ()
219+ onException req ex
220+ | Just (_ :: WS. ConnectionException ) <- fromException ex
221+ = return () -- ignore websocket disconnects
222+ | otherwise = Warp. defaultOnException req ex
223+
224+ staticServer :: Wai. Application
225+ staticServer req respond =
226+ case Wai. pathInfo req of
227+ [] ->
228+ respond $ Wai. responseLBS status200
229+ [(hContentType, " text/html" )]
230+ indexPage
231+ [" js" , " index.js" ] ->
232+ respond $ Wai. responseLBS status200
233+ [(hContentType, " application/javascript" )]
234+ indexJS
235+ [" js" , " latest.js" ] -> do
236+ may <- readTVarIO indexJs
237+ case may of
238+ Nothing ->
239+ respond $ Wai. responseLBS status503 [] " Service not available"
240+ Just js ->
241+ respond $ Wai. responseLBS status200
242+ [ (hContentType, " application/javascript" )
243+ , (hCacheControl, " no-cache, no-store, must-revalidate" )
244+ , (hPragma, " no-cache" )
245+ , (hExpires, " 0" )
246+ ]
247+ (fromString js)
248+ [" js" , " bundle.js" ] -> do
249+ may <- readTVarIO bundleJs
250+ case may of
251+ Nothing ->
252+ respond $ Wai. responseLBS status503 [] " Service not available"
253+ Just js ->
254+ respond $ Wai. responseLBS status200
255+ [ (hContentType, " application/javascript" )]
256+ (fromString js)
257+ _ -> respond $ Wai. responseLBS status404 [] " Not found"
258+
259+ let browserState = BrowserState cmdChan shutdownVar indexJs bundleJs
260+ createBundle browserState
261+
262+ putStrLn $ " Serving http://localhost:" <> show serverPort <> " /. Waiting for connections..."
263+ _ <- forkIO $ Warp. runSettings ( Warp. setInstallShutdownHandler shutdownHandler
264+ . Warp. setPort serverPort
265+ . Warp. setOnException onException
266+ $ Warp. defaultSettings
267+ ) $
268+ WS. websocketsOr WS. defaultConnectionOptions
269+ handleWebsocket
270+ staticServer
271+ return browserState
272+
273+ createBundle :: BrowserState -> IO ()
274+ createBundle state = do
275+ putStrLn " Bundling Javascript..."
276+ ejs <- bundle
277+ case ejs of
278+ Left err -> do
279+ putStrLn (unlines (Bundle. printErrorMessage err))
280+ exitFailure
281+ Right js -> do
282+ atomically $ writeTVar (browserBundleJS state) (Just js)
283+
284+ reload :: BrowserState -> IO ()
285+ reload state = do
286+ createBundle state
287+ atomically $ writeTChan (browserCommands state) Reload
288+
289+ shutdown :: BrowserState -> IO ()
290+ shutdown state = putMVar (browserShutdownNotice state) ()
291+
292+ evaluate :: BrowserState -> String -> IO ()
293+ evaluate state js = liftIO $ do
294+ resultVar <- newEmptyMVar
295+ atomically $ do
296+ writeTVar (browserIndexJS state) (Just js)
297+ writeTChan (browserCommands state) (Eval resultVar)
298+ result <- takeMVar resultVar
299+ putStrLn result
300+
301+ nodeBackend :: [String ] -> Backend
302+ nodeBackend nodeArgs = Backend setup eval reload shutdown
303+ where
304+ setup :: IO ()
305+ setup = return ()
306+
307+ eval :: () -> String -> IO ()
308+ eval _ _ = do
309+ writeFile indexFile " require('$PSCI')['$main']();"
310+ process <- findNodeProcess
311+ result <- traverse (\ node -> readProcessWithExitCode node (nodeArgs ++ [indexFile]) " " ) process
312+ case result of
313+ Just (ExitSuccess , out, _) -> putStrLn out
314+ Just (ExitFailure _, _, err) -> putStrLn err
315+ Nothing -> putStrLn " Couldn't find node.js"
316+
317+ reload :: () -> IO ()
318+ reload _ = return ()
319+
320+ shutdown :: () -> IO ()
321+ shutdown _ = return ()
322+
95323-- | Get command line options and drop into the REPL
96324main :: IO ()
97325main = getOpt >>= loop
@@ -106,27 +334,31 @@ main = getOpt >>= loop
106334 exitFailure
107335 (externs, env) <- ExceptT . runMake . make $ modules
108336 return (modules, externs, env)
109- case e of
110- Left errs -> putStrLn (P. prettyPrintMultipleErrors P. defaultPPEOptions errs) >> exitFailure
111- Right (modules, externs, env) -> do
112- historyFilename <- getHistoryFilename
113- let settings = defaultSettings { historyFile = Just historyFilename }
114- initialState = PSCiState [] [] (zip (map snd modules) externs)
115- config = PSCiConfig inputFiles psciInputNodeFlags env
116- runner = flip runReaderT config
117- . flip evalStateT initialState
118- . runInputT (setComplete completion settings)
119- putStrLn prologueMessage
120- runner go
121- where
122- go :: InputT (StateT PSCiState (ReaderT PSCiConfig IO )) ()
123- go = do
124- c <- getCommand (not psciMultiLineMode)
125- case c of
126- Left err -> outputStrLn err >> go
127- Right Nothing -> go
128- Right (Just QuitPSCi ) -> outputStrLn quitMessage
129- Right (Just c') -> do
130- handleInterrupt (outputStrLn " Interrupted." )
131- (withInterrupt (lift (handleCommand c')))
132- go
337+ case psciBackend of
338+ Backend setup eval reload (shutdown :: state -> IO () ) -> do
339+ case e of
340+ Left errs -> putStrLn (P. prettyPrintMultipleErrors P. defaultPPEOptions errs) >> exitFailure
341+ Right (modules, externs, env) -> do
342+ historyFilename <- getHistoryFilename
343+ let settings = defaultSettings { historyFile = Just historyFilename }
344+ initialState = PSCiState [] [] (zip (map snd modules) externs)
345+ config = PSCiConfig inputFiles env
346+ runner = flip runReaderT config
347+ . flip evalStateT initialState
348+ . runInputT (setComplete completion settings)
349+
350+ go :: state -> InputT (StateT PSCiState (ReaderT PSCiConfig IO )) ()
351+ go state = do
352+ c <- getCommand (not psciMultiLineMode)
353+ case c of
354+ Left err -> outputStrLn err >> go state
355+ Right Nothing -> go state
356+ Right (Just QuitPSCi ) -> do
357+ outputStrLn quitMessage
358+ liftIO $ shutdown state
359+ Right (Just c') -> do
360+ handleInterrupt (outputStrLn " Interrupted." )
361+ (withInterrupt (lift (handleCommand (liftIO . eval state) (liftIO (reload state)) c')))
362+ go state
363+ putStrLn prologueMessage
364+ setup >>= runner . go
0 commit comments