Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non zero exit code on error #27

Merged
merged 2 commits into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions src/Fornax/Fornax.fs
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,42 @@ let main argv =

0
| Some Build ->
Generator.generateFolder cwd
0
try
let results = Generator.generateFolder cwd
0
with
| FornaxGeneratorException message ->
Console.WriteLine message
1
| exn ->
printfn "An unexpected error happend: %s%s%s" exn.Message Environment.NewLine exn.StackTrace
1
| Some Watch ->
let mutable lastAccessed = Map.empty<string, DateTime>
generateFolder cwd
let waitingForChangesMessage = "Generated site with errors. Waiting for changes..."

let guardedGenerate () =
try
generateFolder cwd
with
| FornaxGeneratorException message ->
printfn "%s%s%s" message Environment.NewLine waitingForChangesMessage
| exn ->
printfn "An unexpected error happend: %s%s%s" exn.Message Environment.NewLine exn.StackTrace
exit 1

guardedGenerate ()

use watcher = createFileWatcher cwd (fun e ->
if not (e.FullPath.Contains "_public") && not (e.FullPath.Contains ".sass-cache") && not (e.FullPath.Contains ".git") then
if not (e.FullPath.Contains "_public") && not (e.FullPath.Contains ".sass-cache") && not (e.FullPath.Contains ".git") && not (e.FullPath.Contains ".ionide") then
let lastTimeWrite = File.GetLastWriteTime(e.FullPath)
match lastAccessed.TryFind e.FullPath with
| Some lt when Math.Abs((lt - lastTimeWrite).Seconds) < 1 -> ()
| _ ->
printfn "[%s] Changes detected: %s" (DateTime.Now.ToString("HH:mm:ss")) e.FullPath
lastAccessed <- lastAccessed.Add(e.FullPath, lastTimeWrite)
Generator.generateFolder cwd)
guardedGenerate ())

startWebServerAsync defaultConfig (router cwd) |> snd |> Async.Start
printfn "[%s] Watch mode started. Press any key to exit." (DateTime.Now.ToString("HH:mm:ss"))
Console.ReadKey() |> ignore
Expand Down
167 changes: 117 additions & 50 deletions src/Fornax/Generator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module internal Utils =
cache := (!cache).Add(x,res)
res

let memoizeScriptFile f =
let memoizeScriptFile (f : string -> Result<'a, string>) =
let resultCache = ref Map.empty
let contentCache = ref Map.empty
fun (x : string) ->
Expand All @@ -54,22 +54,28 @@ module internal Utils =
let pth = Path.Combine(dir, e)
[yield! acc; yield! getContent pth ]) contetnMap'
else contetnMap'
let ctn = getContent x

match (!resultCache).TryFind(x) with
| Some res ->
match (!contentCache).TryFind(x) with
| Some r when r = ctn -> res
| _ ->
try
let ctn = getContent x

match (!resultCache).TryFind(x) with
| Some res ->
match (!contentCache).TryFind(x) with
| Some r when r = ctn -> res
| _ ->
let res = f x
resultCache := (!resultCache).Add(x,res)
contentCache := (!contentCache).Add(x,ctn)
res
| None ->
let res = f x
resultCache := (!resultCache).Add(x,res)
contentCache := (!contentCache).Add(x,ctn)
res
| None ->
let res = f x
resultCache := (!resultCache).Add(x,res)
contentCache := (!contentCache).Add(x,ctn)
res
with
| :? FileNotFoundException as fnf ->
Error fnf.Message
| _ -> reraise ()

module Evaluator =
open System.Globalization
Expand All @@ -80,7 +86,7 @@ module Evaluator =

let private sbOut = StringBuilder()
let private sbErr = StringBuilder()
let private fsi =
let internal fsi () =
let inStream = new StringReader("")
let outStream = new StringWriter(sbOut)
let errStream = new StringWriter(sbErr)
Expand Down Expand Up @@ -135,46 +141,71 @@ module Evaluator =
let genExpr = input.ReflectionValue :?> Quotations.Expr
QuotationEvaluator.CompileUntyped genExpr

let private getContentFromLayout' (layoutPath : string) =
let private getContentFromLayout' (fsi : FsiEvaluationSession) (layoutPath : string) =
let filename = getOpen layoutPath
let load = getLoad layoutPath

let _, errs = fsi.EvalInteractionNonThrowing(sprintf "#load \"%s\";;" load)
if errs.Length > 0 then printfn "[ERROR 1] Load Errors : %A" errs

let _, errs = fsi.EvalInteractionNonThrowing(sprintf "open %s;;" filename)
if errs.Length > 0 then printfn "[ERROR 2] Open Errors : %A" errs

let modelType, errs = fsi.EvalExpressionNonThrowing "typeof<Model>"
if errs.Length > 0 then printfn "[ERROR 3] Get model Errors : %A" errs

let siteModelType, errs = fsi.EvalExpressionNonThrowing "typeof<SiteModel.SiteModel>"
if errs.Length > 0 then printfn "[ERROR 4] Get site model Errors : %A" errs

let funType,errs = fsi.EvalExpressionNonThrowing "<@@ fun a b c d -> (generate a b (Post.Construct c) d) |> HtmlElement.ToString @@>"
if errs.Length > 0 then printfn "[ERROR 5] Get layout Errors : %A" errs
let tryFormatErrorMessage message (errors : 'a []) =
if errors.Length > 0 then
sprintf "%s: %A" message errors |> Some
else
None

let _, loadErrors = fsi.EvalInteractionNonThrowing(sprintf "#load \"%s\";;" load)
let loadErrorMessage =
tryFormatErrorMessage "Load Errors" loadErrors

let _, openErrors = fsi.EvalInteractionNonThrowing(sprintf "open %s;;" filename)
let openErrorMessage =
tryFormatErrorMessage "Open Errors" openErrors

let modelType, modelErrors = fsi.EvalExpressionNonThrowing "typeof<Model>"
let modelErrorMesage =
tryFormatErrorMessage "Get model Errors" modelErrors

let siteModelType, siteModelErrors = fsi.EvalExpressionNonThrowing "typeof<SiteModel.SiteModel>"
let siteModelErrorMessage =
tryFormatErrorMessage "Get site model Errors" siteModelErrors

let funType, layoutErrors = fsi.EvalExpressionNonThrowing "<@@ fun a b c d -> (generate a b (Post.Construct c) d) |> HtmlElement.ToString @@>"
let layoutErrorMessage =
tryFormatErrorMessage "Get layout Errors" layoutErrors

let completeErrorReport =
[ loadErrorMessage
openErrorMessage
modelErrorMesage
siteModelErrorMessage
layoutErrorMessage ]
|> List.filter (Option.isSome)
|> List.map (fun v -> v.Value)
|> List.fold (fun state message -> state + Environment.NewLine + message) ""
|> (fun s -> s.Trim(Environment.NewLine.ToCharArray()))

match modelType, siteModelType, funType with
| Choice1Of2 (Some mt), Choice1Of2 (Some smt), Choice1Of2 (Some ft) ->
Some (mt, smt, ft)
| _ -> None

let private getContentFromLayout = Utils.memoizeScriptFile getContentFromLayout'
Ok (mt, smt, ft)
| _ -> Error completeErrorReport

// let private getContentFromLayout = Utils.memoizeScriptFile getContentFromLayout'
let private getContentFromLayout = getContentFromLayout'

///`layoutPath` - absolute path to `.fsx` file containing the layout
///`getSiteModel` - function generating instance of site settings model of given type
///`getContentModel` - function generating instance of page mode of given type
///`body` - content of the post (in html)
let evaluate posts (layoutPath : string) (getSiteModel : System.Type -> obj) (getContentModel : System.Type -> obj * string) =
match getContentFromLayout layoutPath with
| Some (mt, smt, ft) ->
let evaluate (fsi : FsiEvaluationSession) posts (layoutPath : string) (getSiteModel : System.Type -> obj) (getContentModel : System.Type -> obj * string) =
getContentFromLayout fsi layoutPath
|> Result.bind (fun (mt, smt, ft) ->
let modelInput, body = getContentModel (mt.ReflectionValue :?> Type)
let siteInput = getSiteModel (smt.ReflectionValue :?> Type)
let generator = compileExpression ft

invokeFunction generator [siteInput; modelInput; box posts; box body]
|> Option.bind (tryUnbox<string>)
| _ -> None
|> function
| Some s -> Ok s
| None -> sprintf "The expression for %s couldn't be compiled" layoutPath |> Error)

// Module to print colored message in the console
module Logger =
Expand All @@ -184,6 +215,7 @@ module Logger =
{ new IDisposable with
member x.Dispose() = Console.ForegroundColor <- current }

let informationfn str = Printf.kprintf (fun s -> use c = consoleColor ConsoleColor.Green in printfn "%s" s) str
let error str = Printf.kprintf (fun s -> use c = consoleColor ConsoleColor.Red in printf "%s" s) str
let errorfn str = Printf.kprintf (fun s -> use c = consoleColor ConsoleColor.Red in printfn "%s" s) str

Expand Down Expand Up @@ -296,7 +328,6 @@ let private parseLess : string -> string = Utils.memoize StyleParser.parseLess
let private trimString (str : string) =
str.Trim().TrimEnd('"').TrimStart('"')


let getPosts (projectRoot : string) =
let postsPath = Path.Combine(projectRoot, "posts")
Directory.GetFiles postsPath
Expand Down Expand Up @@ -337,6 +368,15 @@ let getPosts (projectRoot : string) =

((link:Link), (title:Title), (author:Author), (published:Published), (tags:Tags), (content:Content)))

exception FornaxGeneratorException of string

type GeneratorMessage = string

type GeneratorResult =
| GeneratorIgnored
| GeneratorSuccess of GeneratorMessage option
| GeneratorFailure of GeneratorMessage

///`projectRoot` - path to the root of website
///`page` - path to page that should be generated
let generate posts (projectRoot : string) (page : string) =
Expand All @@ -357,24 +397,34 @@ let generate posts (projectRoot : string) (page : string) =
let modelLoader = contentParser contentText
let layoutPath = Path.Combine(projectRoot, "layouts", layout + ".fsx")

let result = Evaluator.evaluate posts layoutPath settingsLoader modelLoader
use fsiSession = Evaluator.fsi ()
let result = Evaluator.evaluate fsiSession posts layoutPath settingsLoader modelLoader

match result with
| Some r ->
| Ok r ->
let dir = Path.GetDirectoryName outputPath
if not (Directory.Exists dir) then Directory.CreateDirectory dir |> ignore
File.WriteAllText(outputPath, r)
let endTime = DateTime.Now
let ms = (endTime - startTime).Milliseconds
printfn "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
| None ->
sprintf "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
|> Some
|> GeneratorSuccess
| Error message ->
let endTime = DateTime.Now
printfn "[%s] '%s' generation failed" (endTime.ToString("HH:mm:ss")) outputPath
sprintf "[%s] '%s' generation failed" (endTime.ToString("HH:mm:ss")) outputPath
|> (fun s -> message + Environment.NewLine + s)
|> GeneratorFailure
else
let r = compileMarkdown contentText
let dir = Path.GetDirectoryName outputPath
if not (Directory.Exists dir) then Directory.CreateDirectory dir |> ignore
File.WriteAllText(outputPath, r)
let endTime = DateTime.Now
let ms = (endTime - startTime).Milliseconds
sprintf "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
|> Some
|> GeneratorSuccess

///`projectRoot` - path to the root of website
///`path` - path to file that should be copied
Expand All @@ -384,6 +434,7 @@ let copyStaticFile (projectRoot : string) (path : string) =
let dir = Path.GetDirectoryName outputPath
if not (Directory.Exists dir) then Directory.CreateDirectory dir |> ignore
File.Copy(inputPath, outputPath, true)
GeneratorSuccess None

///`projectRoot` - path to the root of website
///`path` - path to `.less` file that should be copied
Expand All @@ -398,10 +449,12 @@ let generateFromLess (projectRoot : string) (path : string) =
File.WriteAllText(outputPath, res)
let endTime = DateTime.Now
let ms = (endTime - startTime).Milliseconds
printfn "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
sprintf "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
|> Some
|> GeneratorSuccess

///`projectRoot` - path to the root of website
///`path` - path to `.less` file that should be copied
///`path` - path to `.scss` or `.sass` file that should be copied
let generateFromSass (projectRoot : string) (path : string) =
let startTime = DateTime.Now
let inputPath = Path.Combine(projectRoot, path)
Expand All @@ -422,16 +475,19 @@ let generateFromSass (projectRoot : string) (path : string) =
proc.WaitForExit()
let endTime = DateTime.Now
let ms = (endTime - startTime).Milliseconds
printfn "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
sprintf "[%s] '%s' generated in %dms" (endTime.ToString("HH:mm:ss")) outputPath ms
|> Some
|> GeneratorSuccess
with
| :? System.ComponentModel.Win32Exception as ex ->
let endTime = DateTime.Now
Logger.error "[%s] Generation of '%s' failed. " (endTime.ToString("HH:mm:ss")) path'
Logger.errorfn "Please check you have installed the Sass compiler if you are going to be using files with extension .scss. https://sass-lang.com/install"
sprintf "[%s] Generation of '%s' failed. " (endTime.ToString("HH:mm:ss")) path'
|> fun s -> s + Environment.NewLine + "Please check you have installed the Sass compiler if you are going to be using files with extension .scss. https://sass-lang.com/install"
|> GeneratorFailure

let private (|Ignored|Markdown|Less|Sass|StaticFile|) (filename : string) =
let ext = Path.GetExtension filename
if filename.Contains "_public" || filename.Contains "_bin" || filename.Contains "_lib" || filename.Contains "_data" || filename.Contains "_settings" || filename.Contains "_config.yml" || ext = ".fsx" || filename.Contains ".sass-cache" || filename.Contains ".git" then Ignored
if filename.Contains "_public" || filename.Contains "_bin" || filename.Contains "_lib" || filename.Contains "_data" || filename.Contains "_settings" || filename.Contains "_config.yml" || ext = ".fsx" || filename.Contains ".sass-cache" || filename.Contains ".git" || filename.Contains ".ionide" then Ignored
elif ext = ".md" then Markdown
elif ext = ".less" then Less
elif ext = ".sass" || ext =".scss" then Sass
Expand All @@ -450,11 +506,22 @@ let generateFolder (projectRoot : string) =

let posts = getPosts projectRoot

let logResult (result : GeneratorResult) =
match result with
| GeneratorIgnored -> ()
| GeneratorSuccess None -> ()
| GeneratorSuccess (Some message) ->
Logger.informationfn "%s" message
| GeneratorFailure message ->
// if one generator fails we want to exit early and report the problem to the operator
raise (FornaxGeneratorException message)

Directory.GetFiles(projectRoot, "*", SearchOption.AllDirectories)
|> Array.iter (fun n ->
match n with
| Ignored -> ()
| Ignored -> GeneratorIgnored
| Markdown -> n |> relative projectRoot |> generate posts projectRoot
| Less -> n |> relative projectRoot |> generateFromLess projectRoot
| Sass -> n |> relative projectRoot |> generateFromSass projectRoot
| StaticFile -> n |> relative projectRoot |> copyStaticFile projectRoot )
| StaticFile -> n |> relative projectRoot |> copyStaticFile projectRoot
|> logResult)