packages feed

kitchen-sink-0.1.0.0: src/KitchenSink/Engine/Produce.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module KitchenSink.Engine.Produce where

import Control.Exception (SomeAsyncException (..), SomeException, displayException, fromException, throwIO, try)
import Control.Monad (when)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text qualified as Text
import Data.Time.Clock (getCurrentTime)
import Prod.Tracer
import System.Exit (exitFailure)
import System.IO (BufferMode (LineBuffering), hPutStrLn, hSetBuffering, stderr, stdout)
import Prelude ((||))

import KitchenSink.Core.Build.Target (Target, destination, destinationUrl)
import KitchenSink.Engine.Diagnostics (hasFailures, reportDiagnostics)
import KitchenSink.Engine.Runtime
import KitchenSink.Engine.SiteBuilder (produceTarget)
import KitchenSink.Engine.SiteConfig
import KitchenSink.Engine.SiteLoader as SiteLoader
import KitchenSink.Engine.Track (DevServerTrack (..))
import KitchenSink.Engine.Utils
import KitchenSink.Layout.Blog as Blog
import KitchenSink.Prelude

data Args
    = Args
    { srcDir :: FilePath
    , outDir :: Maybe FilePath
    , ksFile :: Maybe FilePath
    , variables :: [(Text, Text)]
    , abortOnError :: Bool
    -- ^ stop at the first target that fails to produce (default: report it, keep producing the rest, exit non-zero at the end)
    }

run :: Args -> IO ()
run cmd = do
    -- progress goes to stdout and problems to stderr: keep their lines whole when both land in one log
    hSetBuffering stdout LineBuffering
    let srcPath = cmd.srcDir
    let kitchensinkFilePath = kitshenSinkJsonFilePath cmd.srcDir cmd.ksFile
    serveMetadata <- loadMetadata kitchensinkFilePath
    let handleLoadSite =
            loadSite
                cmd.variables
                serveMetadata.pathPrefix
                (extraSectiontypes Blog.layout)
                (runTracer $ contramap Loading $ tracePrint)
                srcPath
    let prodengine =
            Engine
                (handleLoadSite)
                (pure serveMetadata)
                (\med site -> fmap (fmap $ const ()) $ (siteTargets Blog.layout) Nothing (fromMaybe "./out" cmd.outDir) med site)
                (produceTarget print)
    site <- execLoadSite prodengine
    meta <- execLoadMetaExtradata prodengine
    let diagnostics = siteDiagnostics Blog.layout site
    reportDiagnostics diagnostics
    let tgts = evalTargets prodengine meta site
    failedTargets <-
        if cmd.abortOnError
            then traverse_ (execProduceTarget prodengine) tgts >> pure []
            else catMaybes <$> traverse (produceOrReport prodengine) tgts
    -- everything that could be produced has been; still fail the command
    when (hasFailures diagnostics || not (null failedTargets)) $ do
        when (not (null failedTargets)) $
            hPutStrLn stderr (show (length failedTargets) <> " target(s) failed to produce; use --abortOnError to stop at the first one")
        exitFailure

-- | Produces one target; a failure is reported on stderr (and returned) instead of ending the run.
produceOrReport :: Engine () -> Target () () -> IO (Maybe Text)
produceOrReport engine tgt = do
    r <- try @SomeException (execProduceTarget engine tgt)
    case r of
        Right () -> pure Nothing
        Left e
            | Just (SomeAsyncException _) <- fromException e -> throwIO e
            | otherwise -> do
                let url = destinationUrl (destination tgt)
                hPutStrLn stderr (Text.unpack url <> ": error: " <> displayException e)
                pure (Just url)

loadMetadata :: FilePath -> IO MetaData
loadMetadata path = do
    config <- fromMaybe defaultSiteInfo <$> loadJSONFile @SiteInfo path
    serveMetadataFromSiteInfo config

serveMetadataFromSiteInfo :: SiteInfo -> IO MetaData
serveMetadataFromSiteInfo config = do
    MetaData
        <$> getCurrentTime
        <*> pure (title config)
        <*> pure (publishURL config)
        <*> pure (twitterLogin config)
        <*> pure noExtraHeaders
        <*> pure (maybe [] (fmap baseURL) $ linkedSites config)
        <*> pure (normalizedBasePath config)
        <*> pure (resolveHomeLink config)
        <*> pure (resolveMenu config)
        <*> pure (resolveFooter config)
  where
    noExtraHeaders _ = pure mempty