module Development.Duplo.Scripts where
import Control.Applicative ((<$>), (<*>))
import Control.Exception (SomeException (..),
throw)
import Control.Lens hiding (Action)
import Control.Monad (filterM)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT (..))
import Data.Function (on)
import Data.List (intercalate, nubBy)
import Data.Text.Format (left)
import Data.Text.Lazy (Text, pack, replace,
splitOn, unpack)
import Data.Text.Lazy.Builder (toLazyText)
import Development.Duplo.Component (extractCompVersions)
import qualified Development.Duplo.Component as CM
import Development.Duplo.Files (File (..), pseudoFile)
import Development.Duplo.JavaScript.Order (order)
import qualified Development.Duplo.Types.Config as TC
import Development.Duplo.Types.JavaScript
import Development.Duplo.Utilities (CompiledContent,
compile, createIntermediaryDirectories,
expandDeps,
expandPaths,
headerPrintSetter,
logStatus)
import Development.Shake
import Development.Shake.FilePath ((</>))
import qualified Language.JavaScript.Parser as JS
import Language.JavaScript.Parser.SrcLocation (TokenPosn (..))
import Text.Regex (matchRegex, mkRegex)
errorDisplayRange :: Int
errorDisplayRange = 20
build :: TC.BuildConfig
-> FilePath
-> CompiledContent ()
build config out = do
liftIO $ logStatus headerPrintSetter "Building scripts"
let cwd = config ^. TC.cwd
let util = config ^. TC.utilPath
let env = config ^. TC.env
let mode = config ^. TC.mode
let buildMode = config ^. TC.buildMode
let input = config ^. TC.input
let devPath = config ^. TC.devPath
let depsPath = config ^. TC.depsPath
let devCodePath = devPath </> "modules/index.js"
let inDev = TC.isInDev config
let inTest = TC.isInTest config
let depIds = config ^. TC.dependencies
lift $ createIntermediaryDirectories devCodePath
dependencies <- liftIO $ CM.getDependencies $ case mode of
"" -> Nothing
a -> Just a
let makeDepId = unpack . replace "/" "-" . pack
let depIds = map makeDepId dependencies
let staticPaths = case buildMode of
"development" -> [ "dev/index" ]
"test" -> [ "test/index" ]
_ -> []
++ [ "app/index" ]
let depsToExpand id = [ "components/" ++ id ++ "/app/modules" ]
let dynamicPaths = [ "app/modules" ]
++ case buildMode of
"development" -> [ "dev/modules" ]
_ -> []
++ expandDeps depIds depsToExpand
paths <- lift $ expandPaths cwd ".js" staticPaths dynamicPaths
let duploIn = if not (null input) then input else ""
compVers <- lift $ extractCompVersions config
let envVars = "var DUPLO_ENV = '" ++ env ++ "';\n"
++ "var DUPLO_IN = JSON.parse(window.atob('" ++ duploIn ++ "') || '{}' );\n"
++ "var DUPLO_VERSIONS = " ++ compVers ++ ";\n"
let compiler = (util </>) $ case buildMode of
"development" -> "scripts-dev.sh"
"test" -> "scripts-test.sh"
_ -> "scripts-optimize.sh"
let pre = return . (:) (pseudoFile { _fileContent = envVars })
let prepareJs = JS.renderToString . order
let post content = return
$ either (handleParseError content) prepareJs
$ JS.parse content ""
compiled <- compile config compiler [] paths pre post
lift $ writeFileChanged out compiled
handleParseError :: String -> String -> String
handleParseError content e = exception
where
linedContent = fmap unpack $ splitOn "\n" $ pack content
lineCount = length linedContent
lineNum = readParseError e
lineRange = take errorDisplayRange
$ iterate (+ 1)
$ lineNum errorDisplayRange `div` 2
showBadLine' = showBadLine linedContent lineNum
keepInRange = max 0 . min lineCount
badLines = fmap (showBadLine' . keepInRange) lineRange
dedupe = nubBy ((==) `on` fst)
badLinesDeduped = map snd $ dedupe badLines
exception = throw
ShakeException { shakeExceptionTarget = ""
, shakeExceptionStack = []
, shakeExceptionInner = SomeException
$ ParseException
badLinesDeduped
}
showBadLine :: [String] -> LineNumber -> LineNumber -> (LineNumber, String)
showBadLine allLines badLineNum lineNum = (lineNum, line')
where
line = allLines !! lineNum
toString = unpack . toLazyText
lineNum' = toString $ left 4 ' ' $ lineNum + 1
marker = if lineNum == badLineNum
then ">> " ++ lineNum'
else " " ++ lineNum'
line' = marker ++ " | " ++ line
readParseError :: String -> LineNumber
readParseError e =
case match of
Just m -> (read $ head m) :: Int
Nothing -> throw $ InternalParserException e
where
regex = mkRegex "TokenPn [0-9]+ ([0-9]+) [0-9]+"
match = matchRegex regex e