diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014 Maciej Pietrzak, https://github.com/mpietrzak/yesod-purescript
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/Yesod/PureScript.hs b/Yesod/PureScript.hs
new file mode 100644
--- /dev/null
+++ b/Yesod/PureScript.hs
@@ -0,0 +1,446 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE ScopedTypeVariables   #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TupleSections         #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE ViewPatterns          #-}
+{-# OPTIONS_GHC -fno-warn-orphans  #-}
+
+
+module Yesod.PureScript (
+    PureScriptSite,
+    YesodPureScript,
+    YesodPureScriptOptions(..),
+    createYesodPureScriptSite,
+    defaultYesodPureScriptOptions,
+    getPureScriptRoute
+ )
+where
+
+-- PureScript Yesod Subsite
+-- goal is to serve eg ./purs/foo.purs file at /purs/foo.js url.
+
+import Control.Exception (catch, SomeException)
+import Control.Monad (forever, forM, forM_)
+import Control.Monad.IO.Class (liftIO)
+import Data.Either (rights)
+import Data.List (isSuffixOf)
+import Data.Maybe (catMaybes, mapMaybe)
+import Data.Text (Text)
+import Data.Time (UTCTime, getCurrentTime)
+import Formatting
+import Formatting.Time
+import Language.PureScript (Module(Module))
+import Prelude
+import System.FilePath ((</>))
+import Text.Julius
+import Yesod.Core ( HandlerT
+                  , Route
+                  , TypedContent (TypedContent)
+                  , Yesod
+                  , YesodSubDispatch
+                  , getYesod
+                  , mkYesodSubDispatch
+                  , shamlet
+                  , toContent
+                  , toTypedContent
+                  , yesodSubDispatch )
+import qualified Control.Concurrent as C
+import qualified Control.Concurrent.MVar as CM
+import qualified Data.Aeson.Types as AesonTypes
+import qualified Data.Default as DD
+import qualified Data.Map.Strict as M
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Text.IO as TIO
+import qualified Data.Text.Lazy as TL
+import qualified Filesystem as FS
+import qualified Filesystem.Path as FSP
+import qualified Filesystem.Path.CurrentOS as FSPC
+import qualified Language.PureScript as P
+import qualified System.Directory as D
+import qualified System.FSNotify as SFN
+import qualified System.IO.UTF8 as U
+
+import Yesod.PureScript.Data
+
+
+-- | Things that are Yesod master can also be YesodPureScript master.
+class Yesod master => YesodPureScript master
+
+
+-- | All things that are "YesodPureScript master", are also this other thing,
+-- because they /nobody knows, because TH/.
+instance YesodPureScript master => YesodSubDispatch PureScriptSite (HandlerT master IO) where
+    yesodSubDispatch = $(mkYesodSubDispatch resourcesPureScriptSite)
+
+
+-- | YesodPureScriptOptions thing is also Data.Default.Default, because it has "def",
+-- which is defaultYesodPureScriptOptions.
+instance DD.Default YesodPureScriptOptions where
+    def = defaultYesodPureScriptOptions
+
+
+-- | Shortcut type for handlers of this subsite.
+type PureScriptHandler a = (YesodPureScript master) => HandlerT PureScriptSite (HandlerT master IO) a
+
+
+-- | Default options for YesodPureScript.
+-- Needed when creating PureScriptSite.
+-- Please don't create YesodPureScriptOptions by calling constructor directly,
+-- so I can add more options without breaking your code.
+defaultYesodPureScriptOptions :: YesodPureScriptOptions
+defaultYesodPureScriptOptions = YesodPureScriptOptions
+        { ypsoSourceDirectories = ["purs", "bower_components"]
+        , ypsoErrorDivId = Nothing
+        , ypsoVerboseErrors = False
+        , ypsoMode = Dynamic
+        , ypsoCompileOptions = [] }
+
+
+-- | Create pure script site.
+-- Initialises MVar of compiled modules to empty map.
+createYesodPureScriptSite :: YesodPureScriptOptions -> IO PureScriptSite
+createYesodPureScriptSite opts = do
+    let state = PureScriptSiteState { psssWatchStarted = False
+                                    , psssModules = M.empty
+                                    , psssCompiledModules = M.empty }
+    mv <- CM.newMVar state
+    return $ PureScriptSite { pssState = mv
+                            , pssOptions = opts }
+
+
+-- | For convenience: turns a path as list into a route.
+getPureScriptRoute :: [Text] -> Route PureScriptSite
+getPureScriptRoute p = PureScriptCompiledR p
+
+
+-- | Create JS error report that tries to insert itself into parent page.
+createJavaScriptError :: TL.Text -> TL.Text -> TL.Text
+createJavaScriptError errorDivId errorText = renderJavascriptUrl render tmpl
+    where
+        render _ _ = error "no links supported here"
+        _s _tl = AesonTypes.String (TL.toStrict _tl)
+        tmpl = [julius|
+                var err = #{_s errorText};
+                var errorDiv = document.getElementById(#{_s errorDivId});
+                if (window && window.console && window.console.log) {
+                    window.console.log(err);
+                }
+                if (errorDiv) {
+                    var errnode = document.createTextNode(err);
+                    var prenode = document.createElement("pre");
+                    prenode.appendChild(errnode);
+                    if (errorDiv.firstChild) {
+                        errorDiv.insertBefore(prenode, errorDiv.firstChild);
+                    } else {
+                        errorDiv.appendChild(errnode);
+                    }
+                }
+            |]
+
+
+getPureScriptInfo :: PureScriptSite -> PureScriptHandler TypedContent
+getPureScriptInfo site = do
+    -- map of filename to either err module
+    moduleMap <- liftIO $ CM.withMVar (pssState site) $ \state -> return (psssModules state)
+
+    -- this is to filter map items whose values are tuples of (a0, Either ...),
+    -- and return those items with keys as tuples of (key, (a0, unboxed right)).
+    -- so this is pattern match on snd of tuple that either gives tuple of unboxed right in just or nothing...
+    let _justSndRight (_k, (_t, _mv)) = case _mv of
+            Right _v -> Just (_k, (_t, _v))
+            _ -> Nothing
+
+    -- similar to _justSndRight above, but returns value from Left (or nothing)
+    let _justSndLeft (_k, (_t, _mv)) = case _mv of
+            Left _v -> Just (_k, (_t, _v))
+            _ -> Nothing
+
+    -- (fn, [module])
+    let fnsmodules = mapMaybe _justSndRight (M.toAscList moduleMap) :: [(Text, (UTCTime, [Module]))]
+
+    -- (fileName, error)
+    let fnerrs = mapMaybe _justSndLeft (M.toAscList moduleMap) :: [(Text, (UTCTime, Text))]
+
+    let _formatTime _t = format (dateDash % " " % hms) _t _t
+
+    return $ toTypedContent $ [shamlet|
+        $doctype 5
+        <html>
+            <style>
+                body {
+                    font-family: sans-serif;
+                    font-size: 10pt;
+                }
+            <body>
+                <h1>yesod-purescript Status
+
+                <h2>Failed Modules
+
+                $case fnerrs
+                    $of []
+                        <p>All modules loaded without errors
+                    $of _
+                        <table>
+                            <thead>
+                                <tr>
+                                    <th>File name
+                                    <th>Error message
+                                    <th>Load time
+                            <tbody>
+                                $forall (fn, (time, err)) <- fnerrs
+                                    <tr>
+                                        <td>#{fn}
+                                        <td>#{err}
+                                        <td>#{_formatTime time}
+
+                <h2>Loaded Modules
+
+                $case fnsmodules
+                    $of []
+                        <p>No modules loaded
+                    $of _
+                        <table>
+                            <thead>
+                                <tr>
+                                    <th>Module
+                                    <th>File name
+                                    <th>Load time
+                            <tbody>
+                                $forall fnmods <- fnsmodules
+                                    $with (fn, (time, modules)) <- fnmods
+                                        $forall (Module name _ _) <- modules
+                                            <tr>
+                                                <td>#{show name}
+                                                <td>#{fn}
+                                                <td>#{_formatTime time}
+        |]
+    where
+
+
+
+-- | This is the main handler, "entry point" for YesodPureScript.
+-- It takes a path to module as a list of Text, then it parses and
+-- compiles stuff and returns results to client.
+getPureScriptCompiledR :: [Text] -> PureScriptHandler TypedContent
+getPureScriptCompiledR [] = do
+    me <- getYesod
+    liftIO $ ensureWatchStarted me
+    getPureScriptInfo me
+
+getPureScriptCompiledR p = do
+    me <- getYesod
+    liftIO $ ensureWatchStarted me
+    let jsModulePath = T.intercalate "." p  -- foo.bar.baz[.js]
+    let jsModuleName = if T.isSuffixOf ".js" jsModulePath
+            then T.dropEnd 3 jsModulePath
+            else jsModulePath
+    compileResult <- liftIO $ compilePureScriptFile me jsModuleName
+    case compileResult of
+        Left err -> do
+            case ypsoErrorDivId (pssOptions me) of
+                Nothing -> do
+                    let errbs = T.encodeUtf8 err
+                    return (TypedContent "text/plain" (toContent errbs))
+                Just _id -> do
+                    let _errtxt = TL.fromStrict err
+                    let _errjs = createJavaScriptError (TL.fromStrict _id) _errtxt
+                    return (TypedContent "text/javascript" (toContent _errjs))
+        Right _js -> do
+            let _jsbs = T.encodeUtf8 _js
+            return (TypedContent "application/javascript" (toContent _jsbs))
+
+
+-- | Called by file-watching thread after loading module from purs file.
+-- Updates in-memory cache of modules.
+-- Result of compilation is either textual representation of parse errors or module.
+-- Cache of loaded modules is key-ed by purs filename - this way we can efficiently handle
+-- file deletions or renames.
+addModule :: PureScriptSite -> Text -> (UTCTime, Either Text [P.Module]) -> IO ()
+addModule pureScriptSite fileName eitherErrOrModules = do
+    CM.modifyMVar_ (pssState pureScriptSite) $ \state -> do
+        let curmap = psssModules state
+        let newmap = M.insert fileName eitherErrOrModules curmap
+        -- For now re-loading of module causes cache all compiled modules to be
+        -- dropped.
+        let newstate = state { psssModules = newmap
+                             , psssCompiledModules = M.empty }
+        return newstate
+
+
+-- | Executed when file disappears from watched dirs,
+-- removes module from PureScriptSite's state.
+removeModule :: PureScriptSite -> Text -> IO ()
+removeModule pureScriptSite fileName = do
+    CM.modifyMVar_ (pssState pureScriptSite) $ \state -> do
+        let curmap = psssModules state
+        let newmap = M.delete fileName curmap
+        let newstate = state { psssModules = newmap
+                             , psssCompiledModules = M.empty }
+        return newstate
+
+
+
+-- | Executed on file change. Updates loaded modules MVar-ed in PureScriptSite.
+handleFileEvent :: PureScriptSite -> SFN.Event -> IO ()
+handleFileEvent pureScriptSite event = do
+        let fp = SFN.eventPath event
+        let mext = FSP.extension fp
+        let _upsert = do
+                _parsed <- parseFile (fp2t fp)
+                _now <- getCurrentTime
+                addModule pureScriptSite (fp2t fp) (_now, _parsed)
+        case (event, mext) of
+            (SFN.Added _ _, Just "purs") -> _upsert
+            (SFN.Modified _ _, Just "purs") -> _upsert
+            (SFN.Removed _ _, Just "purs") -> do
+                removeModule pureScriptSite (fp2t fp)
+            _ -> do
+                -- ignore this event
+                return ()
+    where
+        fp2t fp = case FSPC.toText fp of
+            Left _ -> error "invalid path"
+            Right _t -> _t
+
+
+-- | Start file-watching stuff if not already started.
+ensureWatchStarted :: PureScriptSite -> IO ()
+ensureWatchStarted pureScriptSite = do
+    let mode = ypsoMode $ pssOptions pureScriptSite
+    case mode of
+        Dynamic -> do
+            CM.modifyMVar_ (pssState pureScriptSite) $ \state -> do
+                case psssWatchStarted state of
+                    False -> do
+                        _m <- parseAllFiles pureScriptSite
+                        startWatchThread pureScriptSite
+                        return (state { psssWatchStarted = True
+                                      , psssModules = _m
+                                      , psssCompiledModules = M.empty })
+                    _ -> return state
+        Static -> error "YPS mode is Static, can't start watch thread"
+
+
+-- | Creates thread that watches input dirs and fires events on files.
+-- This way modified files are instantly loaded, making dev setup more responsive.
+startWatchThread :: PureScriptSite -> IO ()
+startWatchThread pureScriptSite = do
+        _ <- C.forkIO $ do
+            let opts = pssOptions pureScriptSite
+            let dirs = ypsoSourceDirectories opts
+            SFN.withManager $ \mgr -> do
+                forM_ dirs $ \dir -> do
+                    SFN.watchTree mgr (FSPC.fromText dir) (const True) $ \e -> do
+                        catch
+                            (handleFileEvent pureScriptSite e)
+                            (\_e -> do
+                                -- XXX: catching all is bad
+                                let msg = show (_e :: SomeException)
+                                TIO.putStrLn $ T.concat ["exception in handleFileEvent: ", T.pack msg])
+                forever $ do
+                    C.threadDelay (12 * 3600 * 1000 * 1000)
+        return ()
+
+
+-- | Take human-readable dir name, find '*.purs' files recursively.
+findFiles :: Text -> IO [Text]
+findFiles dir = do
+    let dirp = T.unpack dir
+    allNames <- D.getDirectoryContents dirp
+    let goodNames = filter (flip notElem [".", ".."]) allNames
+    pathLists <- forM goodNames $ \n -> do
+        let p = dirp </> n
+        isDir <- D.doesDirectoryExist p
+        if isDir
+            then findFiles (T.pack p)
+            else return $ if isSuffixOf ".purs" p then [T.pack p] else []
+    let paths = (concat pathLists)
+    return paths
+
+
+-- | High level parse interface for PureScript.
+-- Takes file path as Text, returns either error as Text or parsed modules.
+-- Note: PureScript file can define more than one module.
+parseFile :: Text -> IO (Either Text [P.Module])
+parseFile fn = do
+    -- TIO.putStrLn $ T.concat ["parsing \"", fn, "\""]
+    fileContents <- U.readFile (T.unpack fn)
+    let eem = case P.lex (T.unpack fn) fileContents of
+            Right _tokens -> P.runTokenParser (T.unpack fn) P.parseModules _tokens
+            Left _err -> Left _err
+    let r = case eem of
+            Left _e -> Left . T.pack . show $ _e
+            Right m -> Right m
+    -- TIO.putStrLn $ T.concat ["parsed \"", fn, "\""]
+    return r
+
+
+-- | Parse modules, return map of path -> parse result: either parse error or parsed modules.
+-- This beautiful code tries to make sure that paths are absolute.
+-- Current impl uses Filesystem.FilePath in hopes that it's faster than String-based impl and
+-- that it handles edge cases better.
+parseAllFiles :: PureScriptSite -> IO (M.Map Text (UTCTime, Either Text [P.Module]))
+parseAllFiles pureScriptSite = do
+    let sourceDirs = ypsoSourceDirectories $ pssOptions pureScriptSite
+    let lsActions = map (\d -> findFiles d) sourceDirs
+    dirsFiles <- sequence lsActions
+    let relFileNames = map FSPC.fromText $ concat dirsFiles
+    cwd <- FS.getWorkingDirectory
+    let absFileNames = map (FSP.append cwd) relFileNames
+    mParseResults <- forM absFileNames $ \afn -> do
+        case FSPC.toText afn of
+            Left _ -> return Nothing
+            Right _t -> do
+                _time <- getCurrentTime
+                _parsed <- parseFile _t
+                return $ Just (_t, (_time, _parsed))
+    let parseResults = catMaybes mParseResults
+    return $ M.fromList $ parseResults
+
+
+-- | Takes PureScriptSite and module name and tries to compile given module in "--main <module>" mode.
+-- PureScriptSite contains parsed modules.
+-- Compiled module is stored (cached) in PureScriptSite's state with time of compilation.
+-- Only compile result is returned.
+-- Cached and returned result of compilation has type of Text.
+compilePureScriptFile :: PureScriptSite -> Text -> IO (Either Text Text)
+compilePureScriptFile pureScriptSite moduleName = do
+    let compileOptions = P.CompileOptions "PS" [T.unpack moduleName] []
+    let psOptions = P.defaultCompileOptions { P.optionsMain = Just (T.unpack moduleName)
+                                            , P.optionsNoPrelude = False
+                                            , P.optionsAdditional = compileOptions
+                                            , P.optionsVerboseErrors = ypsoVerboseErrors (pssOptions pureScriptSite) }
+    compileResult <- CM.modifyMVar (pssState pureScriptSite) $ \state -> do
+        let _m = psssCompiledModules state
+        case M.lookup moduleName _m of
+            Just (_t, _cmt) -> do
+                TIO.putStrLn $ T.concat ["compile result for js module \"", moduleName, "\" found in cache"]
+                return (state, _cmt)
+            Nothing -> do
+                TIO.putStrLn $ T.concat ["compiling js module \"", moduleName, "\""]
+                _time <- getCurrentTime
+                -- No cached compile result in map, need to actually compile.
+                let _lmm = psssModules state
+                let _preludeModules = case P.lex "" P.prelude of
+                        Right _tokens -> case P.runTokenParser "" P.parseModules _tokens of
+                            Right _ms -> _ms
+                            Left _err -> []
+                        Left _err -> []
+                let _loadedModules = concat $ rights $ map snd $ M.elems _lmm
+                let _modules = concat [_preludeModules, _loadedModules]
+                let compileResultRaw = P.compile psOptions _modules ["yesod-purescript"]
+                let compileResult = case compileResultRaw of
+                        Left errStr -> Left (T.pack errStr)
+                        Right (_js, _, _) -> Right (T.pack _js)
+                let newmap = M.insert moduleName (_time, compileResult) _m
+                let newstate = state { psssCompiledModules = newmap }
+                return (newstate, compileResult)
+    return compileResult
+
diff --git a/Yesod/PureScript/Data.hs b/Yesod/PureScript/Data.hs
new file mode 100644
--- /dev/null
+++ b/Yesod/PureScript/Data.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns #-}
+
+module Yesod.PureScript.Data
+where
+
+
+import Control.Concurrent (MVar)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import Yesod.Core
+
+import qualified Data.Map.Strict as M
+import qualified Language.PureScript as P
+
+
+data Mode = Dynamic | Static
+
+
+-- | Per-module compile options.
+data CompileOptions = CompileOptions { coMain :: Maybe Text }
+
+
+-- | Options for Yesod PureScript.
+data YesodPureScriptOptions = YesodPureScriptOptions {
+        -- | Mode of compilation: dynamic or static.
+        ypsoMode :: Mode,
+        -- | Source directories to look for ".purs" files.
+        ypsoSourceDirectories :: [Text],
+        -- | Defines what and how to compile.
+        ypsoCompileOptions :: [CompileOptions],
+        -- | Optionally specifies div id in parent page that will be used to display compilation error if any.
+        -- This is because usually resulting JS is loaded by HTML page, and it might
+        -- save a time (esp for trivial errors) if error is displayed directly in HTML.
+        ypsoErrorDivId :: Maybe Text,
+        -- | Option passed to PureScript compiler, makes errors verbose.
+        ypsoVerboseErrors :: Bool
+    }
+
+
+-- | This is meant to be stored in synchronized variable.
+data PureScriptSiteState = PureScriptSiteState {
+        -- | A flag telling us if compiler subthread is running.
+        psssWatchStarted :: Bool,
+
+        -- | Results of file parsing, time and either errors as text or list of modules,
+        -- keyed by file path.
+        psssModules :: M.Map Text (UTCTime, (Either Text [P.Module])),
+
+        -- | Cached modules compiled with "module" and "main" options to Text.
+        -- Result of compilation is either compile error(s) as Text or
+        -- the Text of compiled module.
+        -- Values in map are tuples of time of compilation attempt and compilation result.
+        psssCompiledModules :: M.Map Text (UTCTime, (Either Text Text))
+    }
+
+
+-- | Yesod sub site for PureScript.
+data PureScriptSite = PureScriptSite { pssState :: MVar PureScriptSiteState
+                                     , pssOptions :: YesodPureScriptOptions }
+
+
+mkYesodSubData "PureScriptSite" [parseRoutes|
+/*Texts PureScriptCompiledR GET
+|]
diff --git a/yesod-purescript.cabal b/yesod-purescript.cabal
new file mode 100644
--- /dev/null
+++ b/yesod-purescript.cabal
@@ -0,0 +1,42 @@
+name:                yesod-purescript
+version:             0.0.4.1
+synopsis:            PureScript integration for Yesod
+description:         PureScript is an awesome statically typed language that compiles to JS and runs in your browser, and this module makes it easier to use PureScript in Yesod.
+homepage:            https://github.com/mpietrzak/yesod-purescript
+license:             MIT
+license-file:        LICENSE
+author:              Maciej Pietrzak
+maintainer:          mpietrzak@gmail.com
+category:            Web, Yesod
+build-type:          Simple
+cabal-version:       >=1.8
+stability:           experimental
+
+source-repository head
+    type: git
+    location: https://github.com/mpietrzak/yesod-purescript
+
+library
+    exposed-modules: Yesod.PureScript
+    other-modules: Yesod.PureScript.Data
+    build-depends: aeson >= 0.8
+                 , base >= 4 && < 5
+                 , containers
+                 , data-default
+                 , directory
+                 , filepath
+                 , formatting
+                 , fsnotify
+                 , parsec
+                 , purescript >= 0.6.3 && < 0.7
+                 , shakespeare >= 2
+                 , text
+                 , time
+                 , system-filepath
+                 , system-fileio
+                 , template-haskell
+                 , transformers
+                 , utf8-string
+                 , yesod-core
+    ghc-options: -Wall
+
