diff --git a/lib/fcs/FSharp.Compiler.Service.dll b/lib/fcs/FSharp.Compiler.Service.dll index 9c464b0cb3..b2b1ca4b08 100644 Binary files a/lib/fcs/FSharp.Compiler.Service.dll and b/lib/fcs/FSharp.Compiler.Service.dll differ diff --git a/lib/fcs/FSharp.Core.dll b/lib/fcs/FSharp.Core.dll index 5108ce9746..d416f75849 100644 Binary files a/lib/fcs/FSharp.Core.dll and b/lib/fcs/FSharp.Core.dll differ diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index 2fd23861d7..41a56bc466 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -239,6 +239,7 @@ type Runner = RootDir = rootDir Configuration = configuration OutDir = outDir + IsWatch = watch Precompile = precompile PrecompiledLib = precompiledLib SourceMaps = args.FlagEnabled "-s" || args.FlagEnabled "--sourceMaps" diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 36bc193739..08e5abc585 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -6,6 +6,7 @@ open System.Collections.Generic open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Diagnostics open FSharp.Compiler.SourceCodeServices +open FSharp.Compiler.Symbols open Fable open Fable.AST @@ -14,6 +15,20 @@ open Fable.Transforms.State open ProjectCracker module private Util = + type PathResolver with + static member Dummy = + { new PathResolver with + member _.TryPrecompiledOutPath(_sourceDir, _relativePath) = None + member _.GetOrAddDeduplicateTargetDir(_importDir, _addTargetDir) = "" } + + let isImplementationFile (fileName: string) = + fileName.EndsWith(".fs") || fileName.EndsWith(".fsx") + + let caseInsensitiveSet(items: string seq): ISet = + let s = HashSet(items) + for i in items do s.Add(i) |> ignore + s :> _ + let loadType (cliArgs: CliArgs) (r: PluginRef): Type = /// Prevent ReflectionTypeLoadException /// From http://stackoverflow.com/a/7889272 @@ -192,6 +207,7 @@ module private Util = return Ok {| File = com.CurrentFile OutPath = outPath Logs = com.Logs + InlineExprs = Array.empty WatchDependencies = com.WatchDependencies |} with e -> return Error {| File = com.CurrentFile @@ -225,11 +241,6 @@ open Util open FileWatcher open FileWatcherUtil -let caseInsensitiveSet(items: string seq): ISet = - let s = HashSet(items) - for i in items do s.Add(i) |> ignore - s :> _ - type FsWatcher(delayMs: int) = let globFilters = [ "*.fs"; "*.fsi"; "*.fsx"; "*.fsproj" ] let createWatcher () = @@ -292,6 +303,13 @@ type File(normalizedFullPath: string) = sourceHash <- Some h h, lazy source + static member MakeSourceReader (files: File[]) = + let fileDic = + files + |> Seq.map (fun f -> f.NormalizedFullPath, f) |> dict + let sourceReader f = fileDic.[f].ReadSource() + files |> Array.map (fun file -> file.NormalizedFullPath), sourceReader + type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFiles: File array) = member _.CliArgs = cliArgs @@ -304,14 +322,15 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi member _.SourceFiles = sourceFiles member _.SourceFilePaths = sourceFiles |> Array.map (fun f -> f.NormalizedFullPath) member _.FableLibDir = crackerResponse.FableLibDir + member _.FableModulesDir = crackerResponse.FableModulesDir - member _.MakeCompiler(currentFile, project, ?watchDependencies, ?triggeredByDependency) = + member _.MakeCompiler(currentFile, project, ?triggeredByDependency) = let opts = match triggeredByDependency with | Some t -> { cliArgs.CompilerOptions with TriggeredByDependency = t } | None -> cliArgs.CompilerOptions let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - CompilerImpl(currentFile, project, opts, fableLibDir, ?watchDependencies=watchDependencies, ?outDir=cliArgs.OutDir) + CompilerImpl(currentFile, project, opts, fableLibDir, watchDependencies=cliArgs.IsWatch, ?outDir=cliArgs.OutDir) member _.MapSourceFiles(f) = ProjectCracked(cliArgs, crackerResponse, Array.map f sourceFiles) @@ -340,84 +359,162 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi let sourceFiles = result.ProjectOptions.SourceFiles |> Array.map File ProjectCracked(cliArgs, result, sourceFiles) -type ProjectChecked(checker: InteractiveChecker, diagnostics: FSharpDiagnostic array, project: Project) = - - static let makeSourceReader (files: File[]) = - let fileDic = - files - |> Seq.map (fun f -> f.NormalizedFullPath, f) |> dict - let sourceReader f = fileDic.[f].ReadSource() - files |> Array.map (fun file -> file.NormalizedFullPath), sourceReader - - static member Check(checker: InteractiveChecker, projectFile, files: File[], ?lastFile, ?optimize) = async { - Log.always "Started F# compilation..." +type FableCompileResult = Result< + {| File: string; OutPath: string; Logs: Log[]; InlineExprs: (string * InlineExpr)[]; WatchDependencies: string[] |}, + {| File: string; Exception: exn |} +> + +type FableCompilerMsg = + | GetFableProject of replyChannel: AsyncReplyChannel + | StartCompilation of filesToCompile: string[] * pathResolver: PathResolver * isTriggeredByDependency: (string -> bool) * replyChannel: AsyncReplyChannel<(* fsharpLogs *) Log[] * FableCompileResult list> + | FSharpFileTypeChecked of FSharpImplementationFileContents + | FSharpCompilationFinished of FSharpCheckProjectResults + | FableFileCompiled of string * FableCompileResult + +type FableCompilerState = { + FableProj: Project + PathResolver: PathResolver + TriggeredByDependency: string -> bool + FilesToCompile: Set + FilesCheckedButNotCompiled: Set + FableFilesToCompileExpectedCount: int + FableFilesCompiledCount: int + FSharpLogs: Log[] + FableResults: FableCompileResult list + HasFSharpCompilationFinished: bool + HasFableCompilationFinished: bool + ReplyChannel: AsyncReplyChannel<(* fsharpLogs *) Log[] * FableCompileResult list> option +} with + static member Create(fableProj, filesToCompile: string[], ?pathResolver, ?triggeredByDependency, ?replyChannel) = + { + FableProj = fableProj + PathResolver = defaultArg pathResolver PathResolver.Dummy + TriggeredByDependency = defaultArg triggeredByDependency (fun _ -> false) + FilesToCompile = set filesToCompile + FilesCheckedButNotCompiled = Set.empty + FableFilesToCompileExpectedCount = filesToCompile |> Array.filter isImplementationFile |> Array.length + FableFilesCompiledCount = 0 + FSharpLogs = [||] + FableResults = [] + HasFSharpCompilationFinished = false + HasFableCompilationFinished = false + ReplyChannel = replyChannel + } - let! checkResults, ms = Performance.measureAsync <| fun () -> - let filePaths, sourceReader = makeSourceReader files - checker.ParseAndCheckProject(projectFile, filePaths, sourceReader, ?lastFile=lastFile) +and FableCompiler(projCracked: ProjectCracked, fableProj: Project, checker: InteractiveChecker) = + let agent = + MailboxProcessor.Start(fun agent -> + let rec loop state = async { + match! agent.Receive() with + | GetFableProject channel -> + channel.Reply(state.FableProj) + return! loop state + + | StartCompilation(filesToCompile, pathResolver, isTriggeredByDependency, replyChannel) -> + let state = FableCompilerState.Create(state.FableProj, filesToCompile, pathResolver, isTriggeredByDependency, replyChannel) + async { + let filePaths, sourceReader = File.MakeSourceReader projCracked.SourceFiles + let! results = checker.ParseAndCheckProject( + projCracked.ProjectFile, + filePaths, + sourceReader, + Array.last filesToCompile, + (FSharpFileTypeChecked >> agent.Post)) + FSharpCompilationFinished results |> agent.Post + } + |> Async.Start + return! loop state + + | FSharpFileTypeChecked file -> + // It seems when there's a pair .fsi/.fs the F# compiler gives the .fsi extension to the implementation file + let fileName = file.FileName |> Path.ensureFsExtension + let state = + if not(state.FilesToCompile.Contains(fileName)) then state + else + let fableProj = state.FableProj.Update(file) + async { + let com = projCracked.MakeCompiler(fileName, fableProj, triggeredByDependency = state.TriggeredByDependency(fileName)) + let! res = compileFile projCracked.CliArgs state.PathResolver com + let res = + if not projCracked.CliArgs.Precompile then res + else + res |> Result.map (fun res -> + {| res with InlineExprs = fableProj.GetFileInlineExprs(com) |}) + FableFileCompiled(fileName, res) |> agent.Post + } + |> Async.Start + + { state with FableProj = fableProj + FilesCheckedButNotCompiled = Set.add fileName state.FilesCheckedButNotCompiled } + + return! loop state + + | FSharpCompilationFinished results -> + let state = + { state with FSharpLogs = getFSharpDiagnostics results.Diagnostics + HasFSharpCompilationFinished = true + HasFableCompilationFinished = Set.isEmpty state.FilesCheckedButNotCompiled } + + FableCompiler.CheckIfCompilationIsFinished(state) + return! loop state + + | FableFileCompiled(fileName, result) -> + let state = + let filesCheckedButNotCompiled = Set.remove fileName state.FilesCheckedButNotCompiled + { state with FableResults = result::state.FableResults + FableFilesCompiledCount = state.FableFilesCompiledCount + 1 + FilesCheckedButNotCompiled = filesCheckedButNotCompiled + HasFableCompilationFinished = state.HasFSharpCompilationFinished && Set.isEmpty filesCheckedButNotCompiled } + + Log.inSameLineIfNotCI( + if state.HasFableCompilationFinished then "" + else + let fileName = IO.Path.GetRelativePath(projCracked.CliArgs.RootDir, fileName) + $"Compiled {state.FableFilesCompiledCount}/{state.FableFilesToCompileExpectedCount}: {fileName}" + ) + + FableCompiler.CheckIfCompilationIsFinished(state) + return! loop state + } - Log.always $"F# compilation finished in %i{ms}ms{Log.newLine}" + FableCompilerState.Create(fableProj, [||]) |> loop) - let implFiles = - match optimize with - | Some true -> checkResults.GetOptimizedAssemblyContents().ImplementationFiles - | _ -> checkResults.AssemblyContents.ImplementationFiles + member _.GetFableProject() = + agent.PostAndAsyncReply(GetFableProject) - return implFiles, checkResults.Diagnostics, lazy checkResults.ProjectContext.GetReferencedAssemblies() + member _.StartCompilation(filesToCompile, pathResolver, isTriggeredByDependency) = async { + Log.always "Started compilation..." + let! results, ms = Performance.measureAsync <| fun () -> + agent.PostAndAsyncReply(fun channel -> StartCompilation(filesToCompile, pathResolver, isTriggeredByDependency, channel)) + Log.always $"Compilation finished in %i{ms}ms{Log.newLine}" + return results } - member _.Project = project - member _.Checker = checker - member _.Diagnostics = diagnostics - - member _.CompileToFile(projCracked: ProjectCracked, outFile: string) = async { - //let argv = [| - // "fsc.exe" - // yield! config.ProjectOptions.OtherOptions - // "--nowin32manifest" // See https://github.com/fsharp/fsharp-compiler-docs/issues/755 - // "--out:" + outPath - // yield! config.SourceFilePaths - //|] - //let checker = FSharpChecker.Create(keepAllBackgroundResolutions=false, keepAllBackgroundSymbolUses=false) - //let! result = checker.Compile(argv) - - let filePaths, sourceReader = makeSourceReader projCracked.SourceFiles - let! (diagnostics, exitCode) = checker.Compile(filePaths, sourceReader, outFile) - - return diagnostics, exitCode - } + static member CheckIfCompilationIsFinished(state: FableCompilerState) = + match state.HasFSharpCompilationFinished, state.HasFableCompilationFinished, state.ReplyChannel with + | true, true, Some channel -> + // Fable results are not guaranteed to be in order but revert them to make them closer to the original order + let fableResults = state.FableResults |> List.rev + channel.Reply(state.FSharpLogs, fableResults) + | _ -> () static member Init(projCracked: ProjectCracked) = async { let checker = InteractiveChecker.Create(projCracked.ProjectOptions) - - let! implFiles, errors, assemblies = - ProjectChecked.Check(checker, projCracked.ProjectFile, projCracked.SourceFiles, - optimize=projCracked.CliArgs.CompilerOptions.OptimizeFSharpAst) - - let project, ms = Performance.measure <| fun () -> + let! assemblies = checker.GetImportedAssemblies() + let fableProj = Project.From( projCracked.ProjectFile, - implFiles, - assemblies.Value, + [], + assemblies, ?precompiledInfo = (projCracked.PrecompiledInfo |> Option.map (fun i -> i :> _)), getPlugin = loadType projCracked.CliArgs ) - Log.always($"Fable project created in {ms}ms") - - return ProjectChecked(checker, errors, project) + return FableCompiler(projCracked, fableProj, checker) } - member this.Update(projCracked: ProjectCracked, filesToCompile) = async { - let! implFiles, errors, _ = - ProjectChecked.Check(this.Checker, projCracked.ProjectFile, projCracked.SourceFiles, - optimize = projCracked.CliArgs.CompilerOptions.OptimizeFSharpAst, - lastFile = Array.last filesToCompile) - - let filesToCompile = set filesToCompile - let implFiles = implFiles |> List.filter (fun f -> filesToCompile.Contains(f.FileName)) - let project = this.Project.Update(implFiles) - return ProjectChecked(checker, errors, project) - } + member _.CompileToFile(outFile: string) = + let filePaths, sourceReader = File.MakeSourceReader projCracked.SourceFiles + checker.Compile(filePaths, sourceReader, outFile) type Watcher = { Watcher: FsWatcher @@ -451,12 +548,11 @@ type Watcher = type State = { CliArgs: CliArgs - ProjectCrackedAndChecked: (ProjectCracked * ProjectChecked) option + ProjectCrackedAndFableCompiler: (ProjectCracked * FableCompiler) option WatchDependencies: Map PendingFiles: string[] DeduplicateDic: ConcurrentDictionary - Watcher: Watcher option - HasCompiledOnce: bool } + Watcher: Watcher option } member this.RunProcessEnv = let nodeEnv = @@ -491,12 +587,11 @@ type State = static member Create(cliArgs, ?watchDelay) = { CliArgs = cliArgs - ProjectCrackedAndChecked = None + ProjectCrackedAndFableCompiler = None WatchDependencies = Map.empty Watcher = watchDelay |> Option.map Watcher.Create DeduplicateDic = ConcurrentDictionary() - PendingFiles = [||] - HasCompiledOnce = false } + PendingFiles = [||] } let private getFilesToCompile (state: State) (changes: ISet) (oldFiles: IDictionary option) (projCracked: ProjectCracked) = let pendingFiles = set state.PendingFiles @@ -529,13 +624,13 @@ let private areCompiledFilesUpToDate (state: State) (filesToCompile: string[]) = let private compilationCycle (state: State) (changes: ISet) = async { let cliArgs = state.CliArgs - let projCracked, projChecked, filesToCompile = - match state.ProjectCrackedAndChecked with + let projCracked, fableCompiler, filesToCompile = + match state.ProjectCrackedAndFableCompiler with | None -> let projCracked = ProjectCracked.Init(cliArgs) projCracked, None, projCracked.SourceFilePaths - | Some(projCracked, projChecked) -> + | Some(projCracked, fableCompiler) -> // For performance reasons, don't crack .fsx scripts for every change let fsprojChanged = changes |> Seq.exists (fun c -> c.EndsWith(".fsproj")) @@ -544,9 +639,9 @@ let private compilationCycle (state: State) (changes: ISet) = async { let newProjCracked = ProjectCracked.Init(cliArgs) // If only source files have changed, keep the project checker to speed up recompilation - let projChecked = + let fableCompiler = if oldProjCracked.ProjectOptions.OtherOptions = newProjCracked.ProjectOptions.OtherOptions - then Some projChecked + then Some fableCompiler else None let oldFiles = oldProjCracked.SourceFiles |> Array.map (fun f -> f.NormalizedFullPath, f) |> dict @@ -555,10 +650,10 @@ let private compilationCycle (state: State) (changes: ISet) = async { | true, f -> f | false, _ -> f) let newProjCracked, filesToCompile = getFilesToCompile state changes (Some oldFiles) newProjCracked - newProjCracked, projChecked, filesToCompile + newProjCracked, fableCompiler, filesToCompile else let projCracked, filesToCompile = getFilesToCompile state changes None projCracked - projCracked, Some projChecked, filesToCompile + projCracked, Some fableCompiler, filesToCompile // Update the watcher (it will restart if the fsproj has changed) // so changes while compiling get enqueued @@ -572,66 +667,28 @@ let private compilationCycle (state: State) (changes: ISet) = async { Log.always "Skipped compilation because all generated files are up-to-date!" return state, 0 else - let! projChecked = - match projChecked with - | None -> ProjectChecked.Init(projCracked) - | Some projChecked when Array.isEmpty filesToCompile -> async.Return projChecked - | Some projChecked -> projChecked.Update(projCracked, filesToCompile) - - let logs = getFSharpDiagnostics projChecked.Diagnostics - logErrors cliArgs.RootDir logs - let hasFSharpError = logs |> Array.exists (fun l -> l.Severity = Severity.Error) - - let! dllPathAndCompletor = async { - match hasFSharpError, cliArgs.Precompile, cliArgs.OutDir with - | false, true, Some outDir -> - let dllPath = PrecompiledInfoImpl.GetDllPath(outDir) - let! completor = - projChecked.CompileToFile(projCracked, dllPath) - |> Async.StartChild - - return Some(dllPath, completor) - | _ -> return None - } - - let! logs, outPaths, state = async { - // Skip Fable recompilation if there are F# errors, this prevents bundlers, dev servers, tests... from being triggered - if hasFSharpError && (Option.isNone state.Watcher || state.HasCompiledOnce) then - return logs, Map.empty, { state with PendingFiles = filesToCompile } - else - Log.always "Started Fable compilation..." - - let pathResolver = state.GetPathResolver(?precompiledInfo = projCracked.PrecompiledInfo) - let! results, ms = Performance.measureAsync <| fun () -> - filesToCompile - |> Array.filter (fun file -> file.EndsWith(".fs") || file.EndsWith(".fsx")) - |> Array.map (fun file -> - projCracked.MakeCompiler(file, projChecked.Project, - watchDependencies = Option.isSome state.Watcher, - triggeredByDependency = state.TriggeredByDependency(file, changes)) - |> compileFile cliArgs pathResolver) - |> Async.Parallel - - Log.always $"Fable compilation finished in %i{ms}ms{Log.newLine}" - - let logs, outPaths, watchDependencies = - ((logs, Map.empty, state.WatchDependencies), results) - ||> Array.fold (fun (logs, outPaths, deps) -> function - | Ok res -> - let logs = Array.append logs res.Logs - let outPaths = Map.add res.File res.OutPath outPaths - let deps = Map.add res.File res.WatchDependencies deps - logs, outPaths, deps - | Error e -> - let log = Log.MakeError(e.Exception.Message, fileName=e.File, tag="EXCEPTION") - Log.verbose(lazy e.Exception.StackTrace) - Array.append logs [|log|], outPaths, deps) - - let state = { state with HasCompiledOnce = true - PendingFiles = [||] - WatchDependencies = watchDependencies } - return logs, outPaths, state - } + let! fableCompiler = + match fableCompiler with + | None -> FableCompiler.Init(projCracked) + | Some fableCompiler -> async.Return fableCompiler + + let pathResolver = state.GetPathResolver(?precompiledInfo = projCracked.PrecompiledInfo) + let! fsharpLogs, fableResults = fableCompiler.StartCompilation(filesToCompile, pathResolver, fun f -> state.TriggeredByDependency(f, changes)) + + let logs, watchDependencies = + ((fsharpLogs, state.WatchDependencies), fableResults) + ||> List.fold (fun (logs, deps) -> function + | Ok res -> + let logs = Array.append logs res.Logs + let deps = Map.add res.File res.WatchDependencies deps + logs, deps + | Error e -> + let msg = e.Exception.Message + if Log.isVerbose() then Log.newLine + e.Exception.StackTrace else "" + let log = Log.MakeError(msg, fileName=e.File, tag="EXCEPTION") + Array.append logs [|log|], deps) + + let state = { state with PendingFiles = [||] + WatchDependencies = watchDependencies } // Sometimes errors are duplicated let logs = Array.distinct logs @@ -653,41 +710,51 @@ let private compilationCycle (state: State) (changes: ISet) = async { errorLogs |> Array.iter (formatLog cliArgs.RootDir >> Log.error) let hasError = Array.isEmpty errorLogs |> not + // TODO: If there's an error in assembly generation or serialization we need to prevent that areCompiledFilesUpToDate evals to true later let! exitCode = async { - match hasError, dllPathAndCompletor with - | false, Some(dllPath, completor) -> - Log.always($"Saving precompiled info...") - let _, ms = Performance.measure <| fun _ -> - let inlineExprs = - let file = Array.last filesToCompile - let com = projCracked.MakeCompiler(file, projChecked.Project) - let exprs = projChecked.Project.GetAllInlineExprs(com) - com.Logs |> logErrors cliArgs.RootDir - exprs - - let files = - projChecked.Project.ImplementationFiles |> Map.map (fun k v -> - match Map.tryFind k outPaths with - | Some outPath -> { RootModule = v.RootModule; OutPath = outPath } - | None -> FableError($"Cannot find out path for precompiled file {k}") |> raise) - - PrecompiledInfoImpl.Save( - dllPath = dllPath, - files = files, - inlineExprs = inlineExprs, - compilerOptions = cliArgs.CompilerOptions, - fableLibDir = projCracked.FableLibDir) - - Log.always($"Precompiled info saved in {ms}ms") - - let! (diagnostics, exitCode), ms = Performance.measureAsync <| fun _ -> completor - Log.always($"Waited {ms}ms for assembly generation") - - if exitCode <> 0 then - getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir - - return exitCode - + match hasError, cliArgs.Precompile with + | false, true -> + let outPathsAndInlineExprs = + (Some(Map.empty, []), fableResults) ||> List.fold (fun acc res -> + match acc, res with + | Some(outPaths, inlineExprs), Ok res -> + Some(Map.add res.File res.OutPath outPaths, res.InlineExprs::inlineExprs) + | _ -> None) + + match outPathsAndInlineExprs with + | None -> return 1 + | Some(outPaths, inlineExprs) -> + // Assembly generation is single threaded but I couldn't make it work in parallel with serialization + // (if I use Async.StartChild, assembly generation doesn't seem to start until serialization is finished) + let dllPath = PrecompiledInfoImpl.GetDllPath(projCracked.FableModulesDir) + Log.always("Generating assembly...") + let! (diagnostics, exitCode), ms = Performance.measureAsync <| fun _ -> fableCompiler.CompileToFile(dllPath) + Log.always($"Assembly generated in {ms}ms") + + if exitCode <> 0 then + getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir + return exitCode + else + Log.always($"Saving precompiled info...") + let! fableProj = fableCompiler.GetFableProject() + let _, ms = Performance.measure <| fun _ -> + let inlineExprs = inlineExprs |> List.rev |> Array.concat + + let files = + fableProj.ImplementationFiles |> Map.map (fun k v -> + match Map.tryFind k outPaths with + | Some outPath -> { RootModule = v.RootModule; OutPath = outPath } + | None -> FableError($"Cannot find out path for precompiled file {k}") |> raise) + + PrecompiledInfoImpl.Save( + files = files, + inlineExprs = inlineExprs, + compilerOptions = cliArgs.CompilerOptions, + fableModulesDir = projCracked.FableModulesDir, + fableLibDir = projCracked.FableLibDir) + + Log.always($"Precompiled info saved in {ms}ms") + return 0 | _ -> return 0 } @@ -723,7 +790,7 @@ let private compilationCycle (state: State) (changes: ISet) = async { exitCode, state let state = - { state with ProjectCrackedAndChecked = Some(projCracked, projChecked) + { state with ProjectCrackedAndFableCompiler = Some(projCracked, fableCompiler) PendingFiles = if state.PendingFiles.Length = 0 then errorLogs |> Array.choose (fun l -> l.FileName) |> Array.distinct @@ -732,7 +799,7 @@ let private compilationCycle (state: State) (changes: ISet) = async { return state, exitCode } -type Msg = +type FileWatcherMsg = | Changes of timeStamp: DateTime * changes: ISet let startCompilation state = async { @@ -755,7 +822,7 @@ let startCompilation state = async { | None -> compilationCycle state changes | Some watcher -> let agent = - MailboxProcessor.Start(fun agent -> + MailboxProcessor.Start(fun agent -> let rec loop state = async { match! agent.Receive() with | Changes(timestamp, changes) -> diff --git a/src/Fable.Cli/ProjectCracker.fs b/src/Fable.Cli/ProjectCracker.fs index 5ea0b2a717..0f96ad7547 100644 --- a/src/Fable.Cli/ProjectCracker.fs +++ b/src/Fable.Cli/ProjectCracker.fs @@ -28,24 +28,24 @@ type CacheInfo = FableLibDir: string Timestamp: DateTime } - static member GetPath(fableModulesPath: string) = - IO.Path.Combine(fableModulesPath, "cache_info.json") + static member GetPath(fableModulesPath: string, isDebug: bool) = + IO.Path.Combine(fableModulesPath, $"""project_cracked{if isDebug then "_debug" else ""}.json""") - static member TryRead(fableModulesPath: string): CacheInfo option = + static member TryRead(fableModulesPath: string, isDebug): CacheInfo option = try - CacheInfo.GetPath(fableModulesPath) |> Json.read |> Some + CacheInfo.GetPath(fableModulesPath, isDebug) |> Json.read |> Some with _ -> None - member this.Write(fableModulesPath: string) = - let path = CacheInfo.GetPath(fableModulesPath) + member this.Write(fableModulesPath: string, isDebug) = + let path = CacheInfo.GetPath(fableModulesPath, isDebug) Json.write path this -type CrackerOptions(fableOpts, fableLib, outDir, configuration, exclude, replace, precompiledLib, noCache, noRestore, projFile) = +type CrackerOptions(fableOpts: CompilerOptions, fableLib, outDir, configuration, exclude, replace, precompiledLib, noCache, noRestore, projFile) = let builtDlls = HashSet() let fableModulesDir = CrackerOptions.GetFableModulesDir(projFile, outDir) let cacheInfo = if noCache then None - else CacheInfo.TryRead(fableModulesDir) + else CacheInfo.TryRead(fableModulesDir, fableOpts.DebugMode) member _.CacheInfo = cacheInfo member _.FableModulesDir = fableModulesDir @@ -71,7 +71,7 @@ type CrackerOptions(fableOpts, fableLib, outDir, configuration, exclude, replace Process.runSync projDir "dotnet" ["build"; "-c"; configuration] |> ignore builtDlls.Add(normalizedDllPath) |> ignore - static member GetFableModulesDir(projFile: string, outDir: string option) = + static member GetFableModulesDir(projFile: string, outDir: string option): string = let fableModulesDir = let baseDir = outDir |> Option.defaultWith (fun () -> IO.Path.GetDirectoryName(projFile)) IO.Path.Combine(baseDir, Naming.fableModules) @@ -84,6 +84,7 @@ type CrackerOptions(fableOpts, fableLib, outDir, configuration, exclude, replace type CrackerResponse = { FableLibDir: string + FableModulesDir: string References: string list ProjectOptions: FSharpProjectOptions PrecompiledInfo: PrecompiledInfoImpl option @@ -622,6 +623,7 @@ let getFullProjectOpts (opts: CrackerOptions) = { ProjectOptions = makeProjectOptions opts.ProjFile otherOptions sourcePaths References = cacheInfo.References FableLibDir = cacheInfo.FableLibDir + FableModulesDir = opts.FableModulesDir PrecompiledInfo = precompiledInfo CacheInvalidated = false } @@ -696,10 +698,11 @@ let getFullProjectOpts (opts: CrackerOptions) = Timestamp = DateTime.Now } - cacheInfo.Write(opts.FableModulesDir) + cacheInfo.Write(opts.FableModulesDir, opts.FableOptions.DebugMode) { ProjectOptions = makeProjectOptions opts.ProjFile otherOptions sourcePaths References = projRefs FableLibDir = fableLibDir + FableModulesDir = opts.FableModulesDir PrecompiledInfo = precompiledInfo CacheInvalidated = true } diff --git a/src/Fable.Cli/Util.fs b/src/Fable.Cli/Util.fs index b362f15a80..0c55737ba7 100644 --- a/src/Fable.Cli/Util.fs +++ b/src/Fable.Cli/Util.fs @@ -17,6 +17,7 @@ type CliArgs = { ProjectFile: string RootDir: string OutDir: string option + IsWatch: bool Precompile: bool PrecompiledLib: string option FableLibraryPath: string option @@ -54,6 +55,7 @@ type Agent<'T> private (mbox: MailboxProcessor<'T>, cts: CancellationTokenSource [] module Log = let newLine = Environment.NewLine + let isCi = String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) |> not let mutable private verbosity = Fable.Verbosity.Normal @@ -61,6 +63,19 @@ module Log = let makeVerbose() = verbosity <- Fable.Verbosity.Verbose + let isVerbose() = + verbosity = Fable.Verbosity.Verbose + + let inSameLineIfNotCI (msg: string) = + if not isCi then + let curCursorLeft = Console.CursorLeft + Console.SetCursorPosition(0, Console.CursorTop) + Console.Out.Write(msg) + let diff = curCursorLeft - msg.Length + if diff > 0 then + Console.Out.Write(String.replicate diff " ") + Console.SetCursorPosition(msg.Length, Console.CursorTop) + let alwaysWithColor color (msg: string) = if verbosity <> Fable.Verbosity.Silent && not(String.IsNullOrEmpty(msg)) then Console.ForegroundColor <- color @@ -648,10 +663,10 @@ type PrecompiledInfoJson = Files: Map InlineExprHeaders: string[] } -type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = +type PrecompiledInfoImpl(fableModulesDir: string, info: PrecompiledInfoJson) = let dic = System.Collections.Concurrent.ConcurrentDictionary>>() let comparer = StringOrdinalComparer() - let dllPath = PrecompiledInfoImpl.GetDllPath(dir) + let dllPath = PrecompiledInfoImpl.GetDllPath(fableModulesDir) member _.CompilerVersion = info.CompilerVersion member _.CompilerOptions = info.CompilerOptions @@ -663,8 +678,8 @@ type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = Map.tryFind normalizedFullPath info.Files |> Option.map (fun f -> f.OutPath) - static member GetDllPath(dir: string): string = - IO.Path.Combine(dir, Fable.Naming.fablePrecompile + ".dll") + static member GetDllPath(fableModulesDir: string): string = + IO.Path.Combine(fableModulesDir, Fable.Naming.fablePrecompile + ".dll") |> Fable.Path.normalizeFullPath interface Fable.Transforms.State.PrecompiledInfo with @@ -681,7 +696,7 @@ type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = // http://reedcopsey.com/2011/01/16/concurrentdictionarytkeytvalue-used-with-lazyt/ let map = dic.GetOrAdd(index, fun _ -> lazy - PrecompiledInfoImpl.GetInlineExprsPath(dir, index) + PrecompiledInfoImpl.GetInlineExprsPath(fableModulesDir, index) |> Json.readWithStringPool<(string * Fable.InlineExpr)[]> |> Map) Map.tryFind memberUniqueName map.Value @@ -689,8 +704,8 @@ type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = static member GetPath(dir) = IO.Path.Combine(dir, "precompiled_info.json") - static member GetInlineExprsPath(dir, index: int) = - IO.Path.Combine(dir, "inline_exprs", $"inline_exprs_{index}.json") + static member GetInlineExprsPath(fableModulesDir, index: int) = + IO.Path.Combine(fableModulesDir, "inline_exprs", $"inline_exprs_{index}.json") static member Load(dir: string) = try @@ -700,8 +715,7 @@ type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = with | e -> FableError($"Cannot load precompiled info from %s{dir}: %s{e.Message}") |> raise - static member Save(dllPath: string, files, inlineExprs, compilerOptions, fableLibDir) = - let dir = IO.Path.GetDirectoryName(dllPath) + static member Save(files, inlineExprs, compilerOptions, fableModulesDir, fableLibDir) = let comparer = StringOrdinalComparer() :> System.Collections.Generic.IComparer let inlineExprs = @@ -711,16 +725,16 @@ type PrecompiledInfoImpl(dir: string, info: PrecompiledInfoJson) = |> Array.mapi (fun i chunk -> i, chunk) do - PrecompiledInfoImpl.GetInlineExprsPath(dir, 0) + PrecompiledInfoImpl.GetInlineExprsPath(fableModulesDir, 0) |> IO.Path.GetDirectoryName |> IO.Directory.CreateDirectory |> ignore inlineExprs |> Array.Parallel.iter (fun (i, chunk) -> - let path = PrecompiledInfoImpl.GetInlineExprsPath(dir, i) + let path = PrecompiledInfoImpl.GetInlineExprsPath(fableModulesDir, i) Json.writeWithStringPool path chunk) - let precompiledInfoPath = PrecompiledInfoImpl.GetPath(dir) + let precompiledInfoPath = PrecompiledInfoImpl.GetPath(fableModulesDir) let inlineExprHeaders = inlineExprs |> Array.map (snd >> Array.head >> fst) { CompilerVersion = Fable.Literals.VERSION diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 78499b9c40..d16a36a8a5 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1427,7 +1427,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = { Body = e UsedNames = set ctx.UsedNamesInDeclarationScope }]) -let getRootFSharpEntities (file: FSharpImplementationFileContents) = +let rec getRootFSharpEntities (declarations: FSharpImplementationFileDeclaration list) = let rec getRootFSharpEntitiesInner decl = seq { match decl with | FSharpImplementationFileDeclaration.Entity (ent, nested) -> @@ -1437,9 +1437,9 @@ let getRootFSharpEntities (file: FSharpImplementationFileContents) = else ent | _ -> () } - file.Declarations |> Seq.collect getRootFSharpEntitiesInner + Seq.collect getRootFSharpEntitiesInner declarations -let getRootModule (file: FSharpImplementationFileContents) = +let getRootModule (declarations: FSharpImplementationFileDeclaration list) = let rec getRootModuleInner outerEnt decls = match decls, outerEnt with | [FSharpImplementationFileDeclaration.Entity (ent, decls)], _ when ent.IsFSharpModule || ent.IsNamespace -> @@ -1448,7 +1448,7 @@ let getRootModule (file: FSharpImplementationFileContents) = getRootModuleInner (Some ent) decls | _, Some e -> FsEnt.FullName e | _, None -> "" - getRootModuleInner None file.Declarations + getRootModuleInner None declarations type FableCompiler(com: Compiler) = let attachedMembers = Dictionary() @@ -1639,7 +1639,7 @@ type FableCompiler(com: Compiler) = member _.AddLog(msg, severity, ?range, ?fileName:string, ?tag: string) = com.AddLog(msg, severity, ?range=range, ?fileName=fileName, ?tag=tag) -let getInlineExprs (file: FSharpImplementationFileContents) = +let getInlineExprs fileName (declarations: FSharpImplementationFileDeclaration list) = let rec getInlineExprsInner decls = decls |> List.collect (function @@ -1660,7 +1660,7 @@ let getInlineExprs (file: FSharpImplementationFileContents) = { Args = List.rev idents Body = com.Transform(ctx, body) - FileName = file.FileName + FileName = fileName ScopeIdents = set ctx.UsedNamesInDeclarationScope }) [getMemberUniqueName memb, inlineExpr] @@ -1668,15 +1668,15 @@ let getInlineExprs (file: FSharpImplementationFileContents) = | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue _ | FSharpImplementationFileDeclaration.InitAction _ -> [] ) - getInlineExprsInner file.Declarations + getInlineExprsInner declarations let transformFile (com: Compiler) = - let file = com.GetImplementationFile(com.CurrentFile) - let usedRootNames = getUsedRootNames com Set.empty file.Declarations + let declarations = com.GetImplementationFile(com.CurrentFile) + let usedRootNames = getUsedRootNames com Set.empty declarations let ctx = Context.Create(usedRootNames) let com = FableCompiler(com) let rootDecls = - transformDeclarations com ctx file.Declarations + transformDeclarations com ctx declarations |> List.map (function | Fable.ClassDeclaration decl as classDecl -> com.TryGetAttachedMembers(decl.Entity.FullName) diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 04ceb0550b..4ae5e9c743 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -51,7 +51,7 @@ type Compiler = abstract ProjectFile: string abstract Options: CompilerOptions abstract Plugins: CompilerPlugins - abstract GetImplementationFile: fileName: string -> FSharpImplementationFileContents + abstract GetImplementationFile: fileName: string -> FSharpImplementationFileDeclaration list abstract GetRootModule: fileName: string -> string abstract TryGetEntity: Fable.EntityRef -> Fable.Entity option abstract GetInlineExpr: string -> InlineExpr @@ -61,7 +61,7 @@ type Compiler = type InlineExprLazy(f: Compiler -> InlineExpr) = let mutable value: InlineExpr voption = ValueNone - member this.Force(com: Compiler) = + member this.Calculate(com: Compiler) = lock this <| fun () -> match value with | ValueSome v -> v @@ -69,11 +69,8 @@ type InlineExprLazy(f: Compiler -> InlineExpr) = let v = f com value <- ValueSome v v - [] module CompilerExt = - open System.Collections.Generic - let private expectedVersionMatchesActual (expected: string) (actual: string) = try let r = System.Text.RegularExpressions.Regex(@"^(\d+)\.(\d+)(?:\.(\d+))?") diff --git a/src/Fable.Transforms/Global/Prelude.fs b/src/Fable.Transforms/Global/Prelude.fs index 1b0444ce12..00f0da4eaf 100644 --- a/src/Fable.Transforms/Global/Prelude.fs +++ b/src/Fable.Transforms/Global/Prelude.fs @@ -504,12 +504,14 @@ module Path = normalizePath (GetFullPath path) /// If path belongs to a signature file (.fsi), replace the extension with .fs - let normalizePathAndEnsureFsExtension (path: string) = - let path = normalizePath path + let ensureFsExtension (path: string) = if path.EndsWith(".fsi") then path.Substring(0, path.Length - 1) else path + let normalizePathAndEnsureFsExtension (path: string) = + normalizePath path |> ensureFsExtension + let replaceExtension (newExt: string) (path: string) = let i = path.LastIndexOf(".") if i > 0 then path.Substring(0, i) + newExt diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 54dbb9574b..e045b4d669 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -78,12 +78,13 @@ type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list) = type ImplFile = { - Ast: FSharpImplementationFileContents + Declarations: FSharpImplementationFileDeclaration list RootModule: string Entities: IReadOnlyDictionary InlineExprs: (string * InlineExprLazy) list } static member From(file: FSharpImplementationFileContents) = + let declarations = file.Declarations let entities = Dictionary() let rec loop (ents: FSharpEntity seq) = for e in ents do @@ -93,12 +94,12 @@ type ImplFile = entities.Add(fableEnt.FullName, fableEnt) loop e.NestedEntities - FSharp2Fable.Compiler.getRootFSharpEntities file |> loop + FSharp2Fable.Compiler.getRootFSharpEntities declarations |> loop { - Ast = file + Declarations = file.Declarations Entities = entities - RootModule = FSharp2Fable.Compiler.getRootModule file - InlineExprs = FSharp2Fable.Compiler.getInlineExprs file + RootModule = FSharp2Fable.Compiler.getRootModule declarations + InlineExprs = FSharp2Fable.Compiler.getInlineExprs file.FileName declarations } type PrecompiledInfo = @@ -124,7 +125,7 @@ type Project(projFile: string, member _.TryGetRootModule(_) = None member _.TryGetInlineExpr(_) = None }) - static member From(projFile, + static member From(projFile: string, fsharpFiles: FSharpImplementationFileContents list, fsharpAssemblies: FSharpAssembly list, ?getPlugin: PluginRef -> System.Type, @@ -143,27 +144,24 @@ type Project(projFile: string, Project(projFile, implFilesMap, assemblies, ?precompiledInfo=precompiledInfo) - member this.Update(fsharpFiles: FSharpImplementationFileContents list) = + member this.Update(file: FSharpImplementationFileContents) = let implFiles = - (this.ImplementationFiles, fsharpFiles) ||> List.fold (fun implFiles file -> - let key = Path.normalizePathAndEnsureFsExtension file.FileName - let file = ImplFile.From(file) - Map.add key file implFiles) - + let key = Path.normalizePathAndEnsureFsExtension file.FileName + let file = ImplFile.From(file) + Map.add key file this.ImplementationFiles Project(this.ProjectFile, implFiles, this.Assemblies, this.PrecompiledInfo) member _.TryGetInlineExpr(com: Compiler, memberUniqueName: string) = inlineExprsDic.TryValue(memberUniqueName) - |> Option.map (fun e -> e.Force(com)) + |> Option.map (fun e -> e.Calculate(com)) - member _.GetAllInlineExprs(com: Compiler): (string * InlineExpr)[] = - implFiles - |> Map.values - |> Seq.map (fun f -> f.InlineExprs) - |> Seq.toArray - |> Array.Parallel.map (List.mapToArray (fun (uniqueName, expr) -> - uniqueName, expr.Force(com))) - |> Array.concat + member _.GetFileInlineExprs(com: Compiler): (string * InlineExpr)[] = + match Map.tryFind com.CurrentFile implFiles with + | None -> [||] + | Some implFile -> + implFile.InlineExprs + |> List.mapToArray (fun (uniqueName, expr) -> + uniqueName, expr.Calculate(com)) member _.ProjectFile = projFile member _.ImplementationFiles = implFiles @@ -212,7 +210,7 @@ type CompilerImpl(currentFile, project: Project, options, fableLibraryDir: strin member _.GetImplementationFile(fileName) = let fileName = Path.normalizePathAndEnsureFsExtension fileName match Map.tryFind fileName project.ImplementationFiles with - | Some file -> file.Ast + | Some file -> file.Declarations | None -> failwith ("Cannot find implementation file " + fileName) member this.GetRootModule(fileName) = diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index e5f3a21033..7ca96706d4 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -325,5 +325,5 @@ let init () = let res = results :?> ParseAndCheckResults let project = res.GetProject() let implFile = project.ImplementationFiles.Item(fileName) - AstPrint.printFSharpDecls "" implFile.Ast.Declarations |> String.concat "\n" + AstPrint.printFSharpDecls "" implFile.Declarations |> String.concat "\n" }