ionide / Fornax

Scriptable static site generator using type safe F# DSL to define page templates.
MIT License
241 stars 44 forks source link

The first time a file content changes the new loaded information are not provided to the generator #115

Open MangelMaxime opened 1 year ago

MangelMaxime commented 1 year ago

Describe the bug

To Reproduce Steps to reproduce the behaviour:

  1. Start dotnet fornax watch
  2. Wait for the first generation
  3. Change the content of one of the file
  4. See that the new generated file still have the old content
  5. Change the content of one of the file
  6. See that the new generated file is now using the correct content
  7. Change the content of one of the file
  8. See that the new generated file is now using the correct content
  9. etc. (From now, the generation will always be correct)

Expected behaviour

The file should always used the last loaded information even on the first change.

Screenshots

First generation (all good)

Loader and generator have the same informations

==== POST LOADER =====
"---
layout: post
title: Some nice post title
author: k_cieslak
published: 2020-02-19
tags: [ 'F#', 'blog']
---
# Introduction

First
"
==== POST GENERATOR =====
"# Introduction

First
"
[22:10:56] '/home/mmangel/Workspaces/GitHub/MangelMaxime/mangelmaxime.github.io/_public/posts/first.html' generated in 58ms
Generation time: 00:00:04.6666586
[22:10:56] Watch mode started. Press any key to exit.
[22:10:56 INF] Smooth! Suave listener started in 32.428ms with binding 127.0.0.1:8080

Second generation (not good) : first time user update a file

Loader is using the new file content while the generator still received the old informations.

[22:11:11] Changes detected: /home/mmangel/Workspaces/GitHub/MangelMaxime/mangelmaxime.github.io/posts/first.md
==== POST LOADER =====
"---
layout: post
title: Some nice post title
author: k_cieslak
published: 2020-02-19
tags: [ 'F#', 'blog']
---
# Introduction

File is updated
"
==== POST GENERATOR =====
"# Introduction

First
"
[22:11:12] '/home/mmangel/Workspaces/GitHub/MangelMaxime/mangelmaxime.github.io/_public/posts/first.html' generated in 0ms
Generation time: 00:00:00.7931652

Third and more generation (all good)

Loader and generator have the same informations

[22:12:07] Changes detected: /home/mmangel/Workspaces/GitHub/MangelMaxime/mangelmaxime.github.io/posts/first.md
==== POST LOADER =====
"---
layout: post
title: Some nice post title
author: k_cieslak
published: 2020-02-19
tags: [ 'F#', 'blog']
---
# Introduction

File is updated 2
"
==== POST GENERATOR =====
"# Introduction

File is updated 2
"
[22:12:08] '/home/mmangel/Workspaces/GitHub/MangelMaxime/mangelmaxime.github.io/_public/posts/first.html' generated in 49ms
Generation time: 00:00:01.0151255

Could it be caused by a cache mechanism?

Postloader.fsx ```fs #r "../_lib/Fornax.Core.dll" #load "../.paket/load/main.group.fsx" #load "../utils/Log.fsx" #load "../utils/Helpers.fsx" open System open System.IO open System.Diagnostics open System.Threading.Tasks open Legivel.Serialization open Helpers type Post = { relativeFile: string link : string title: string author: string option published: DateTime option tags: string list content: string } type PostFrontMatter = { title: string author: string option published: DateTime option tags: string list } let contentDir = "posts" let private getLastModified (fileName: string) = async { let psi = ProcessStartInfo() psi.FileName <- "git" psi.Arguments <- $"--no-pager log -1 --format=%%ai \"%s{fileName}\"" psi.RedirectStandardError <- true psi.RedirectStandardOutput <- true psi.CreateNoWindow <- true psi.WindowStyle <- ProcessWindowStyle.Hidden psi.UseShellExecute <- false use p = new Process() p.StartInfo <- psi p.Start() |> ignore let outTask = Task.WhenAll( [| p.StandardOutput.ReadToEndAsync() p.StandardError.ReadToEndAsync() |] ) do! p.WaitForExitAsync() |> Async.AwaitTask let! result = outTask |> Async.AwaitTask if p.ExitCode = 0 then // File is not in the git repo if String.IsNullOrEmpty result[0] then return DateTime.Now else return DateTime.Parse(result[0]) else Log.error $"Failed to get last modified information %s{result[1]}" return DateTime.Now } |> Async.RunSynchronously let private loadFile (rootDir: string) (absolutePath: string) = let text = File.ReadAllText absolutePath printfn "==== POST LOADER =====" printfn "%A" text let relativePath = Path.relativePath rootDir absolutePath let lines = text.Replace("\r\n", "\n").Split("\n") let x = getLastModified absolutePath let firstLine = Array.tryHead lines if firstLine <> Some "---" then Log.error $"File '%s{relativePath}' does not have a front matter" None else let lines = lines |> Array.skip 1 let frontMatterLines = lines |> Array.takeWhile (fun line -> line <> "---") let markdownContent = lines |> Array.skip (frontMatterLines.Length + 1) |> String.concat "\n" let frontMatterContent = frontMatterLines |> String.concat "\n" let frontMatterResult = Deserialize frontMatterContent |> List.head match frontMatterResult with | Error error -> Log.error $"Error parsing front matter in file '%s{relativePath}': %A{error}" None | Success frontMatter -> if not (frontMatter.Warn.IsEmpty) then for warning in frontMatter.Warn do Log.warn $"Warning in file '%s{relativePath}': %A{warning}" let link = Path.ChangeExtension(relativePath, "html") { relativeFile = relativePath link = link title = "" author = None published = frontMatter.Data.published tags = frontMatter.Data.tags content = markdownContent } |> Some let loader (projectRoot: string) (siteContent: SiteContents) = let postsPath = Path.Combine(projectRoot, contentDir) let options = EnumerationOptions(RecurseSubdirectories = true) let files = Directory.GetFiles(postsPath, "*", options) files |> Array.filter (fun n -> n.EndsWith ".md") |> Array.map (loadFile projectRoot) // Only keep the valid post to avoid to propagate errors |> Array.filter Option.isSome |> Array.map Option.get |> Array.iter siteContent.Add siteContent ```
Postloader.fsx ```fs #r "../_lib/Fornax.Core.dll" #load "layout.fsx" open Giraffe.ViewEngine let generate (ctx: SiteContents) (_projectRoot: string) (page: string) = let postOpt = ctx.TryGetValues() |> Option.defaultValue Seq.empty |> Seq.tryFind (fun post -> post.relativeFile = page) match postOpt with | None -> let error = { Path = page Message = $"Post %s{page} not found in the context" Phase = Generating } ctx.AddError error Layout.generationErrorPage ctx | Some post -> printfn "==== POST GENERATOR =====" printfn "%A" post.content div [] [ str post.content ] |> Layout.mainPage ctx ```
drewknab commented 3 months ago

Late on this (incredibly so) but it appears to be an issue with the lastAccessed state map.

It's empty on the first save so the watcher stores the lastAccessed time and the write time to the same value. On the second save the lastTimeWrite and lastAccessed time (in seconds) are equal.

if shouldHandle then
    let lastTimeWrite = File.GetLastWriteTime(e.FullPath)
    match lastAccessed.TryFind e.FullPath with
    | Some lt when Math.Abs((lt - lastTimeWrite).Seconds) < 1 -> ()
    | _ ->
        informationfn "[%s] Changes detected: %s" (DateTime.Now.ToString("HH:mm:ss")) e.FullPath
        lastAccessed <- lastAccessed.Add(e.FullPath, lastTimeWrite)
        guardedGenerate ())

I'll fiddle around with it.