diff --git a/Fable.sln b/Fable.sln index 8adccb6e07..b9aba20c23 100644 --- a/Fable.sln +++ b/Fable.sln @@ -62,6 +62,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Tests.Spaces", "tests EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Build", "src\Fable.Build\Fable.Build.fsproj", "{F2E323CE-FDF3-4A1E-AE97-B723D2E63763}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Compiler", "src\Fable.Compiler\Fable.Compiler.fsproj", "{942DD29B-07C0-4ACF-891E-85C1235A9BE0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -160,6 +162,10 @@ Global {F2E323CE-FDF3-4A1E-AE97-B723D2E63763}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2E323CE-FDF3-4A1E-AE97-B723D2E63763}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2E323CE-FDF3-4A1E-AE97-B723D2E63763}.Release|Any CPU.Build.0 = Release|Any CPU + {942DD29B-07C0-4ACF-891E-85C1235A9BE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {942DD29B-07C0-4ACF-891E-85C1235A9BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {942DD29B-07C0-4ACF-891E-85C1235A9BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {942DD29B-07C0-4ACF-891E-85C1235A9BE0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -189,6 +195,7 @@ Global {F9134F40-C6CD-4368-A28E-33E5102FD4AB} = {CF0A8EC3-841F-4A54-B9FC-8D174CCD4A90} {C90E23AF-4B5B-44A7-ADCC-3BF89547395B} = {DA29278E-3808-42DE-8333-964F129F295D} {F2E323CE-FDF3-4A1E-AE97-B723D2E63763} = {C8CB96CF-68A8-4083-A0F8-319275CF8097} + {942DD29B-07C0-4ACF-891E-85C1235A9BE0} = {C8CB96CF-68A8-4083-A0F8-319275CF8097} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {58DF9285-8523-4EAC-B598-BE5B02A76A00} diff --git a/src/Fable.Build/Publish.fs b/src/Fable.Build/Publish.fs index 8f8826898f..ff4862d52a 100644 --- a/src/Fable.Build/Publish.fs +++ b/src/Fable.Build/Publish.fs @@ -155,6 +155,7 @@ let handle (args: string list) = publishNuget ProjectDir.fableAst publishNuget ProjectDir.fableCore + publishNuget ProjectDir.fableCompiler publishNuget ProjectDir.fableCli publishNuget ProjectDir.fablePublishUtils diff --git a/src/Fable.Build/Workspace.fs b/src/Fable.Build/Workspace.fs index f7f64fa316..bc7179391a 100644 --- a/src/Fable.Build/Workspace.fs +++ b/src/Fable.Build/Workspace.fs @@ -11,6 +11,7 @@ module ProjectDir = let fableCore = Path.Resolve("src", "Fable.Core") let fableCli = Path.Resolve("src", "Fable.Cli") let fablePublishUtils = Path.Resolve("src", "Fable.PublishUtils") + let fableCompiler = Path.Resolve("src", "Fable.Compiler") let temp_fable_library = Path.Resolve("temp", "fable-library") let fable_library = Path.Resolve("src", "fable-library") let fable_metadata = Path.Resolve("src", "fable-metadata") diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index d9394f949a..1d3b6c1f29 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -3,6 +3,7 @@ module Fable.Cli.Entry open System open Main open Fable +open Fable.Compiler.Util type CliArgs(args: string list) = let argsMap = diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index 8efee54df9..c0b1f736fd 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -50,11 +50,7 @@ - - - - @@ -65,6 +61,7 @@ + diff --git a/src/Fable.Cli/FileWatchers.fs b/src/Fable.Cli/FileWatchers.fs index ae951624b9..6bf99809d8 100644 --- a/src/Fable.Cli/FileWatchers.fs +++ b/src/Fable.Cli/FileWatchers.fs @@ -12,7 +12,7 @@ open System.Threading open System.Collections.Generic open System.Diagnostics open System.Text.RegularExpressions -open Fable.Cli.Globbing +open Fable.Compiler.Globbing type IFileSystemWatcher = inherit IDisposable diff --git a/src/Fable.Cli/Globbing.fs b/src/Fable.Cli/Globbing.fs deleted file mode 100644 index 12ba2152d6..0000000000 --- a/src/Fable.Cli/Globbing.fs +++ /dev/null @@ -1,512 +0,0 @@ -namespace Fable.Cli - -open System -open System.Collections.Generic -open System.IO - -/// Globbing support and operators -/// -/// Forked from `Fake.IO.FileSystem` -module Globbing = - /// This module contains a file pattern globbing implementation. - [] - module Glob = - open System - open System.Text.RegularExpressions - - // Normalizes path for different OS - let inline normalizePath (path: string) = - path - .Replace('\\', Path.DirectorySeparatorChar) - .Replace('/', Path.DirectorySeparatorChar) - - type private SearchOption = - | Directory of string - | Drive of string - | Recursive - | FilePattern of string - - let private checkSubDirs absolute (dir: string) root = - if dir.Contains "*" then - try - Directory.EnumerateDirectories( - root, - dir, - SearchOption.TopDirectoryOnly - ) - |> Seq.toList - with :? System.IO.DirectoryNotFoundException -> - List.empty - else - let path = Path.Combine(root, dir) - - let di = - if absolute then - new DirectoryInfo(dir) - else - new DirectoryInfo(path) - - if di.Exists then - [ di.FullName ] - else - [] - - let rec private buildPaths acc (input: SearchOption list) = - match input with - | [] -> acc - | Directory name :: t -> - let subDirs = List.collect (checkSubDirs false name) acc - buildPaths subDirs t - | Drive name :: t -> - let subDirs = List.collect (checkSubDirs true name) acc - buildPaths subDirs t - | Recursive :: [] -> - let dirs = - Seq.collect - (fun dir -> - try - Directory.EnumerateFileSystemEntries( - dir, - "*", - SearchOption.AllDirectories - ) - with :? System.IO.DirectoryNotFoundException -> - Seq.empty - ) - acc - - buildPaths (acc @ Seq.toList dirs) [] - | Recursive :: t -> - let dirs = - Seq.collect - (fun dir -> - try - Directory.EnumerateDirectories( - dir, - "*", - SearchOption.AllDirectories - ) - with :? System.IO.DirectoryNotFoundException -> - Seq.empty - ) - acc - - buildPaths (acc @ Seq.toList dirs) t - | FilePattern pattern :: _ -> - acc - |> List.collect (fun dir -> - if Directory.Exists(Path.Combine(dir, pattern)) then - [ Path.Combine(dir, pattern) ] - else - try - Directory.EnumerateFiles(dir, pattern) - |> Seq.toList - with - | :? System.IO.DirectoryNotFoundException - | :? System.IO.PathTooLongException -> [] - ) - - let private driveRegex = Regex(@"^[A-Za-z]:$", RegexOptions.Compiled) - - let inline private normalizeOutputPath (p: string) = - p - .Replace('\\', Path.DirectorySeparatorChar) - .Replace('/', Path.DirectorySeparatorChar) - .TrimEnd(Path.DirectorySeparatorChar) - - let internal getRoot (baseDirectory: string) (pattern: string) = - let baseDirectory = normalizePath baseDirectory - let normPattern = normalizePath pattern - - let patternParts = - normPattern.Split( - [| - '/' - '\\' - |], - StringSplitOptions.RemoveEmptyEntries - ) - - let patternPathParts = - patternParts - |> Seq.takeWhile (fun p -> not (p.Contains("*"))) - |> Seq.toArray - - let globRoot = - // If we did not find any "*", then drop the last bit (it is a file name, not a pattern) - (if patternPathParts.Length = patternParts.Length then - patternPathParts.[0 .. patternPathParts.Length - 2] - else - patternPathParts) - |> String.concat (Path.DirectorySeparatorChar.ToString()) - - let globRoot = - // If we dropped "/" from the beginning of the path in the 'Split' call, put it back! - if normPattern.StartsWith('/') then - "/" + globRoot - else - globRoot - - if Path.IsPathRooted globRoot then - globRoot - else - Path.Combine(baseDirectory, globRoot) - - let internal search (baseDir: string) (originalInput: string) = - let baseDir = normalizePath baseDir - let input = normalizePath originalInput - - let input = - if String.IsNullOrEmpty baseDir then - input - else - // The final \ (or /) makes sure to only match complete folder - // names (as one folder name could be a substring of the other) - let start = - baseDir.TrimEnd([| Path.DirectorySeparatorChar |]) - + string Path.DirectorySeparatorChar - // See https://github.com/fsharp/FAKE/issues/1925 - if input.StartsWith(start, StringComparison.Ordinal) then - input.Substring start.Length - else - input - - let filePattern = Path.GetFileName(input) - - let splits = - input.Split( - [| - '/' - '\\' - |], - StringSplitOptions.None - ) - - let baseItems = - let start, rest = - if - input.StartsWith("\\\\", StringComparison.Ordinal) - && splits.Length >= 4 - then - let serverName = splits.[2] - let share = splits.[3] - - [ Directory(sprintf "\\\\%s\\%s" serverName share) ], - splits |> Seq.skip 4 - elif - splits.Length >= 2 - && Path.IsPathRooted input - && driveRegex.IsMatch splits.[0] - then - [ Directory(splits.[0] + "\\") ], splits |> Seq.skip 1 - elif - splits.Length >= 2 - && Path.IsPathRooted input - && input.StartsWith '/' - then - [ Directory("/") ], splits |> Array.toSeq - else - if Path.IsPathRooted input then - if input.StartsWith '\\' then - failwithf - "Please remove the leading '\\' or '/' and replace them with \ - '.\\' or './' if you want to use a relative path. Leading \ - slashes are considered an absolute path (input was '%s')!" - originalInput - else - failwithf - "Unknown globbing input '%s', try to use a \ - relative path and report an issue!" - originalInput - - [], splits |> Array.toSeq - - let restList = - rest - |> Seq.filter (String.IsNullOrEmpty >> not) - |> Seq.map ( - function - | "**" -> Recursive - | a when a = filePattern -> FilePattern(a) - | a -> Directory(a) - ) - |> Seq.toList - - start @ restList - - baseItems |> buildPaths [ baseDir ] |> List.map normalizeOutputPath - - let internal compileGlobToRegex pattern = - let pattern = normalizePath pattern - - let escapedPattern = (Regex.Escape pattern) - - let regexPattern = - let xTOy = - [ - "dirwildcard", (@"\\\*\\\*(/|\\\\)", @"(.*(/|\\))?") - "stardotstar", (@"\\\*\\.\\\*", @"([^\\/]*)") - "wildcard", (@"\\\*", @"([^\\/]*)") - ] - |> List.map (fun (key, (pattern, replace)) -> - let pattern = sprintf "(?<%s>%s)" key pattern - key, (pattern, replace) - ) - - let xTOyMap = xTOy |> Map.ofList - - let replacePattern = - xTOy - |> List.map (fun x -> x |> snd |> fst) - |> String.concat ("|") - - let replaced = - Regex(replacePattern) - .Replace( - escapedPattern, - fun m -> - let matched = - xTOy - |> Seq.map (fst) - |> Seq.find (fun n -> - m.Groups.Item(n).Success - ) - - (xTOyMap |> Map.tryFind matched).Value |> snd - ) - - "^" + replaced + "$" - - Regex(regexPattern) - - let private globRegexCache = - System.Collections.Concurrent.ConcurrentDictionary() - - let isMatch pattern path : bool = - let path = normalizePath path - - let regex = - let outRegex: ref = ref null - - if globRegexCache.TryGetValue(pattern, outRegex) then - outRegex.Value - else - let compiled = compileGlobToRegex pattern - globRegexCache.TryAdd(pattern, compiled) |> ignore - compiled - - regex.IsMatch(path) - - type IGlobbingPattern = - inherit IEnumerable - abstract BaseDirectory: string - abstract Includes: string list - abstract Excludes: string list - - type LazyGlobbingPattern = - { - BaseDirectory: string - Includes: string list - Excludes: string list - } - - interface IGlobbingPattern with - member this.BaseDirectory = this.BaseDirectory - member this.Includes = this.Includes - member this.Excludes = this.Excludes - - interface IEnumerable with - - member this.GetEnumerator() = - let hashSet = HashSet() - - let excludes = - seq { - for pattern in this.Excludes do - yield! Glob.search this.BaseDirectory pattern - } - |> Set.ofSeq - - let files = - seq { - for pattern in this.Includes do - yield! Glob.search this.BaseDirectory pattern - } - |> Seq.filter (fun x -> not (Set.contains x excludes)) - |> Seq.filter (fun x -> hashSet.Add x) - - files.GetEnumerator() - - member this.GetEnumerator() = - (this :> IEnumerable).GetEnumerator() - :> System.Collections.IEnumerator - - type ResolvedGlobbingPattern = - { - BaseDirectory: string - Includes: string list - Excludes: string list - Results: string list - } - - interface IGlobbingPattern with - member this.BaseDirectory = this.BaseDirectory - member this.Includes = this.Includes - member this.Excludes = this.Excludes - - interface IEnumerable with - member this.GetEnumerator() = - (this.Results :> IEnumerable).GetEnumerator() - - member this.GetEnumerator() = - (this :> IEnumerable).GetEnumerator() - :> System.Collections.IEnumerator - - [] - module GlobbingPatternExtensions = - type IGlobbingPattern with - - member internal this.Pattern = - match this with - | :? LazyGlobbingPattern as l -> l - | _ -> - { - BaseDirectory = this.BaseDirectory - Includes = this.Includes - Excludes = this.Excludes - } - - member this.Resolve() = - match this with - | :? ResolvedGlobbingPattern as res -> res :> IGlobbingPattern - | _ -> - let list = this |> Seq.toList - - { - BaseDirectory = this.BaseDirectory - Includes = this.Includes - Excludes = this.Excludes - Results = list - } - :> IGlobbingPattern - - /// Adds the given pattern to the file includes - member this.And pattern = - { this.Pattern with Includes = this.Includes @ [ pattern ] } - :> IGlobbingPattern - - /// Ignores files with the given pattern - member this.ButNot pattern = - { this.Pattern with Excludes = pattern :: this.Excludes } - :> IGlobbingPattern - - /// Sets a directory as BaseDirectory. - member this.SetBaseDirectory(dir: string) = - { this.Pattern with - BaseDirectory = dir.TrimEnd(Path.DirectorySeparatorChar) - } - :> IGlobbingPattern - - /// Checks if a particular file is matched - member this.IsMatch(path: string) = - let fullDir (pattern: string) = - if Path.IsPathRooted(pattern) then - pattern - else - System.IO.Path.Combine(this.BaseDirectory, pattern) - - let fullPath = Path.GetFullPath path - - let included = - this.Includes - |> List.exists (fun fileInclude -> - Glob.isMatch (fullDir fileInclude) fullPath - ) - - let excluded = - this.Excludes - |> List.exists (fun fileExclude -> - Glob.isMatch (fullDir fileExclude) fullPath - ) - - included && not excluded - - [] - module GlobbingPattern = - let private defaultBaseDir = Path.GetFullPath "." - - /// Include files - let create x = - { - BaseDirectory = defaultBaseDir - Includes = [ x ] - Excludes = [] - } - :> IGlobbingPattern - - /// Start an empty globbing pattern from the specified directory - let createFrom (dir: string) = - { - BaseDirectory = dir - Includes = [] - Excludes = [] - } - :> IGlobbingPattern - - /// Sets a directory as baseDirectory for fileIncludes. - let setBaseDir (dir: string) (fileIncludes: IGlobbingPattern) = - fileIncludes.SetBaseDirectory dir - - /// Get base include directories. - /// - /// Used to get a smaller set of directories from a globbing pattern. - let getBaseDirectoryIncludes (fileIncludes: IGlobbingPattern) = - let directoryIncludes = - fileIncludes.Includes - |> Seq.map (fun file -> - Glob.getRoot fileIncludes.BaseDirectory file - ) - - // remove subdirectories - directoryIncludes - |> Seq.filter (fun d -> - directoryIncludes - |> Seq.exists (fun p -> - d.StartsWith( - p + string Path.DirectorySeparatorChar, - StringComparison.Ordinal - ) - && p <> d - ) - |> not - ) - |> Seq.toList - - /// Contains operators to find and process files. - /// - /// ### Simple glob using as list - /// - /// let csProjectFiles = !! "src/*.csproj" - /// - /// for projectFile in csProjectFiles do - /// printf "F# ProjectFile: %s" projectFile - /// - /// ### Combine globs - /// - /// let projectFiles = - /// !! "src/*/*.*proj" - /// ++ "src/*/*.target" - /// -- "src/*/*.vbproj" - /// - /// for projectFile in projectFiles do - /// printf "ProjectFile: %s" projectFile - /// - module Operators = - /// Add Include operator - let inline (++) (x: IGlobbingPattern) pattern = x.And pattern - - /// Exclude operator - let inline (--) (x: IGlobbingPattern) pattern = x.ButNot pattern - - /// Includes a single pattern and scans the files - !! x = AllFilesMatching x - let inline (!!) x = GlobbingPattern.create x diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 20c7ba90ba..fe23dfb95e 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -12,7 +12,8 @@ open Fable open Fable.AST open Fable.Transforms open Fable.Transforms.State -open ProjectCracker +open Fable.Compiler.ProjectCracker +open Fable.Compiler.Util module private Util = type PathResolver with @@ -122,7 +123,9 @@ module private Util = let logErrors rootDir (logs: Log seq) = logs |> Seq.filter (fun log -> log.Severity = Severity.Error) - |> Seq.iter (fun log -> Log.error (formatLog rootDir log)) + |> Seq.iter (fun log -> + Fable.Compiler.Util.Log.error (formatLog rootDir log) + ) let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = diagnostics @@ -433,29 +436,12 @@ type FsWatcher(delayMs: int) = |> Observable.throttle delayMs |> Observable.map caseInsensitiveSet -// TODO: Check the path is actually normalized? -type File(normalizedFullPath: string) = - let mutable sourceHash = None - member _.NormalizedFullPath = normalizedFullPath - - member _.ReadSource() = - match sourceHash with - | Some h -> h, lazy File.readAllTextNonBlocking normalizedFullPath - | _ -> - let source = File.readAllTextNonBlocking normalizedFullPath - let h = hash source - 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) + ( + cliArgs: CliArgs, + crackerResponse: CrackerResponse, + sourceFiles: Fable.Compiler.File array + ) = member _.CliArgs = cliArgs @@ -532,7 +518,9 @@ OUTPUT TYPE: {result.OutputType} "Compiling project as Library. If you intend to run the code directly, please set OutputType to Exe." | _ -> () - let sourceFiles = result.ProjectOptions.SourceFiles |> Array.map File + let sourceFiles = + result.ProjectOptions.SourceFiles |> Array.map Fable.Compiler.File + ProjectCracked(cliArgs, result, sourceFiles) type FableCompileResult = @@ -553,7 +541,7 @@ type ReplyChannel = type FableCompilerMsg = | GetFableProject of replyChannel: AsyncReplyChannel | StartCompilation of - sourceFiles: File[] * + sourceFiles: Fable.Compiler.File[] * filesToCompile: string[] * pathResolver: PathResolver * isSilent: bool * @@ -613,9 +601,9 @@ type FableCompilerState = and FableCompiler ( + checker: InteractiveChecker, projCracked: ProjectCracked, - fableProj: Project, - checker: InteractiveChecker + fableProj: Project ) = let agent = @@ -706,7 +694,8 @@ and FableCompiler FSharpCompilationFinished (fun () -> let filePaths, sourceReader = - File.MakeSourceReader sourceFiles + Fable.Compiler.File.MakeSourceReader + sourceFiles let subscriber = if @@ -930,12 +919,12 @@ and FableCompiler getPlugin = loadType projCracked.CliArgs ) - return FableCompiler(projCracked, fableProj, checker) + return FableCompiler(checker, projCracked, fableProj) } member _.CompileToFile(outFile: string) = let filePaths, sourceReader = - File.MakeSourceReader projCracked.SourceFiles + Fable.Compiler.File.MakeSourceReader projCracked.SourceFiles checker.Compile(filePaths, sourceReader, outFile) @@ -1049,7 +1038,7 @@ type State = let private getFilesToCompile (state: State) (changes: ISet) - (oldFiles: IDictionary option) + (oldFiles: IDictionary option) (projCracked: ProjectCracked) = let pendingFiles = set state.PendingFiles @@ -1058,7 +1047,7 @@ let private getFilesToCompile let projCracked = projCracked.MapSourceFiles(fun file -> if changes.Contains(file.NormalizedFullPath) then - File(file.NormalizedFullPath) + Fable.Compiler.File(file.NormalizedFullPath) else file ) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index bf2e7355da..78b309a824 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -4,6 +4,7 @@ open System open Fable open Fable.AST open Fable.Transforms +open Fable.Compiler.Util type Stream = static member WriteToFile(memoryStream: IO.Stream, filePath: string) = diff --git a/src/Fable.Cli/Printers.fs b/src/Fable.Cli/Printers.fs index 70db45760c..809320f15b 100644 --- a/src/Fable.Cli/Printers.fs +++ b/src/Fable.Cli/Printers.fs @@ -3,6 +3,7 @@ module Fable.Cli.Printers open System.IO open FSharp.Compiler.Symbols open Fable +open Fable.Compiler.Util let attribsOfSymbol (s: FSharpSymbol) = [ diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md new file mode 100644 index 0000000000..c7ac6df3a0 --- /dev/null +++ b/src/Fable.Compiler/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +* Initial release diff --git a/src/Fable.Compiler/Fable.Compiler.fsproj b/src/Fable.Compiler/Fable.Compiler.fsproj new file mode 100644 index 0000000000..53e43832bf --- /dev/null +++ b/src/Fable.Compiler/Fable.Compiler.fsproj @@ -0,0 +1,56 @@ + + + + net6.0 + true + true + true + Fable.Compiler + 0.1.0 + + + + + embedded + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Fable.Compiler/File.fs b/src/Fable.Compiler/File.fs new file mode 100644 index 0000000000..724a23f819 --- /dev/null +++ b/src/Fable.Compiler/File.fs @@ -0,0 +1,41 @@ +namespace Fable.Compiler + +open System.IO + +// TODO: Check the path is actually normalized? +type File(normalizedFullPath: string) = + let mutable sourceHash = None + + let readAllTextNonBlocking (path: string) = + if File.Exists(path) then + use fileStream = + new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ) + + use textReader = new StreamReader(fileStream) + textReader.ReadToEnd() + else + // Log.always("File does not exist: " + path) + "" + + member _.NormalizedFullPath = normalizedFullPath + + member _.ReadSource() = + match sourceHash with + | Some h -> h, lazy readAllTextNonBlocking normalizedFullPath + | _ -> + let source = readAllTextNonBlocking normalizedFullPath + let h = hash source + sourceHash <- Some h + h, lazy source + + static member MakeSourceReader(files: File array) = + 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 diff --git a/src/Fable.Compiler/Globbing.fs b/src/Fable.Compiler/Globbing.fs new file mode 100644 index 0000000000..c8c5db7bc2 --- /dev/null +++ b/src/Fable.Compiler/Globbing.fs @@ -0,0 +1,509 @@ +module Fable.Compiler.Globbing + +open System +open System.Collections.Generic +open System.IO + +/// Globbing support and operators +/// +/// Forked from `Fake.IO.FileSystem` +/// +/// This module contains a file pattern globbing implementation. +[] +module Glob = + open System + open System.Text.RegularExpressions + + // Normalizes path for different OS + let inline normalizePath (path: string) = + path + .Replace('\\', Path.DirectorySeparatorChar) + .Replace('/', Path.DirectorySeparatorChar) + + type private SearchOption = + | Directory of string + | Drive of string + | Recursive + | FilePattern of string + + let private checkSubDirs absolute (dir: string) root = + if dir.Contains "*" then + try + Directory.EnumerateDirectories( + root, + dir, + SearchOption.TopDirectoryOnly + ) + |> Seq.toList + with :? System.IO.DirectoryNotFoundException -> + List.empty + else + let path = Path.Combine(root, dir) + + let di = + if absolute then + new DirectoryInfo(dir) + else + new DirectoryInfo(path) + + if di.Exists then + [ di.FullName ] + else + [] + + let rec private buildPaths acc (input: SearchOption list) = + match input with + | [] -> acc + | Directory name :: t -> + let subDirs = List.collect (checkSubDirs false name) acc + buildPaths subDirs t + | Drive name :: t -> + let subDirs = List.collect (checkSubDirs true name) acc + buildPaths subDirs t + | Recursive :: [] -> + let dirs = + Seq.collect + (fun dir -> + try + Directory.EnumerateFileSystemEntries( + dir, + "*", + SearchOption.AllDirectories + ) + with :? System.IO.DirectoryNotFoundException -> + Seq.empty + ) + acc + + buildPaths (acc @ Seq.toList dirs) [] + | Recursive :: t -> + let dirs = + Seq.collect + (fun dir -> + try + Directory.EnumerateDirectories( + dir, + "*", + SearchOption.AllDirectories + ) + with :? System.IO.DirectoryNotFoundException -> + Seq.empty + ) + acc + + buildPaths (acc @ Seq.toList dirs) t + | FilePattern pattern :: _ -> + acc + |> List.collect (fun dir -> + if Directory.Exists(Path.Combine(dir, pattern)) then + [ Path.Combine(dir, pattern) ] + else + try + Directory.EnumerateFiles(dir, pattern) |> Seq.toList + with + | :? System.IO.DirectoryNotFoundException + | :? System.IO.PathTooLongException -> [] + ) + + let private driveRegex = Regex(@"^[A-Za-z]:$", RegexOptions.Compiled) + + let inline private normalizeOutputPath (p: string) = + p + .Replace('\\', Path.DirectorySeparatorChar) + .Replace('/', Path.DirectorySeparatorChar) + .TrimEnd(Path.DirectorySeparatorChar) + + let internal getRoot (baseDirectory: string) (pattern: string) = + let baseDirectory = normalizePath baseDirectory + let normPattern = normalizePath pattern + + let patternParts = + normPattern.Split( + [| + '/' + '\\' + |], + StringSplitOptions.RemoveEmptyEntries + ) + + let patternPathParts = + patternParts + |> Seq.takeWhile (fun p -> not (p.Contains("*"))) + |> Seq.toArray + + let globRoot = + // If we did not find any "*", then drop the last bit (it is a file name, not a pattern) + (if patternPathParts.Length = patternParts.Length then + patternPathParts.[0 .. patternPathParts.Length - 2] + else + patternPathParts) + |> String.concat (Path.DirectorySeparatorChar.ToString()) + + let globRoot = + // If we dropped "/" from the beginning of the path in the 'Split' call, put it back! + if normPattern.StartsWith('/') then + "/" + globRoot + else + globRoot + + if Path.IsPathRooted globRoot then + globRoot + else + Path.Combine(baseDirectory, globRoot) + + let internal search (baseDir: string) (originalInput: string) = + let baseDir = normalizePath baseDir + let input = normalizePath originalInput + + let input = + if String.IsNullOrEmpty baseDir then + input + else + // The final \ (or /) makes sure to only match complete folder + // names (as one folder name could be a substring of the other) + let start = + baseDir.TrimEnd([| Path.DirectorySeparatorChar |]) + + string Path.DirectorySeparatorChar + // See https://github.com/fsharp/FAKE/issues/1925 + if input.StartsWith(start, StringComparison.Ordinal) then + input.Substring start.Length + else + input + + let filePattern = Path.GetFileName(input) + + let splits = + input.Split( + [| + '/' + '\\' + |], + StringSplitOptions.None + ) + + let baseItems = + let start, rest = + if + input.StartsWith("\\\\", StringComparison.Ordinal) + && splits.Length >= 4 + then + let serverName = splits.[2] + let share = splits.[3] + + [ Directory(sprintf "\\\\%s\\%s" serverName share) ], + splits |> Seq.skip 4 + elif + splits.Length >= 2 + && Path.IsPathRooted input + && driveRegex.IsMatch splits.[0] + then + [ Directory(splits.[0] + "\\") ], splits |> Seq.skip 1 + elif + splits.Length >= 2 + && Path.IsPathRooted input + && input.StartsWith '/' + then + [ Directory("/") ], splits |> Array.toSeq + else + if Path.IsPathRooted input then + if input.StartsWith '\\' then + failwithf + "Please remove the leading '\\' or '/' and replace them with \ + '.\\' or './' if you want to use a relative path. Leading \ + slashes are considered an absolute path (input was '%s')!" + originalInput + else + failwithf + "Unknown globbing input '%s', try to use a \ + relative path and report an issue!" + originalInput + + [], splits |> Array.toSeq + + let restList = + rest + |> Seq.filter (String.IsNullOrEmpty >> not) + |> Seq.map ( + function + | "**" -> Recursive + | a when a = filePattern -> FilePattern(a) + | a -> Directory(a) + ) + |> Seq.toList + + start @ restList + + baseItems |> buildPaths [ baseDir ] |> List.map normalizeOutputPath + + let internal compileGlobToRegex pattern = + let pattern = normalizePath pattern + + let escapedPattern = (Regex.Escape pattern) + + let regexPattern = + let xTOy = + [ + "dirwildcard", (@"\\\*\\\*(/|\\\\)", @"(.*(/|\\))?") + "stardotstar", (@"\\\*\\.\\\*", @"([^\\/]*)") + "wildcard", (@"\\\*", @"([^\\/]*)") + ] + |> List.map (fun (key, (pattern, replace)) -> + let pattern = sprintf "(?<%s>%s)" key pattern + key, (pattern, replace) + ) + + let xTOyMap = xTOy |> Map.ofList + + let replacePattern = + xTOy + |> List.map (fun x -> x |> snd |> fst) + |> String.concat ("|") + + let replaced = + Regex(replacePattern) + .Replace( + escapedPattern, + fun m -> + let matched = + xTOy + |> Seq.map (fst) + |> Seq.find (fun n -> m.Groups.Item(n).Success) + + (xTOyMap |> Map.tryFind matched).Value |> snd + ) + + "^" + replaced + "$" + + Regex(regexPattern) + + let private globRegexCache = + System.Collections.Concurrent.ConcurrentDictionary() + + let isMatch pattern path : bool = + let path = normalizePath path + + let regex = + let outRegex: ref = ref null + + if globRegexCache.TryGetValue(pattern, outRegex) then + outRegex.Value + else + let compiled = compileGlobToRegex pattern + globRegexCache.TryAdd(pattern, compiled) |> ignore + compiled + + regex.IsMatch(path) + +type IGlobbingPattern = + inherit IEnumerable + abstract BaseDirectory: string + abstract Includes: string list + abstract Excludes: string list + +type LazyGlobbingPattern = + { + BaseDirectory: string + Includes: string list + Excludes: string list + } + + interface IGlobbingPattern with + member this.BaseDirectory = this.BaseDirectory + member this.Includes = this.Includes + member this.Excludes = this.Excludes + + interface IEnumerable with + + member this.GetEnumerator() = + let hashSet = HashSet() + + let excludes = + seq { + for pattern in this.Excludes do + yield! Glob.search this.BaseDirectory pattern + } + |> Set.ofSeq + + let files = + seq { + for pattern in this.Includes do + yield! Glob.search this.BaseDirectory pattern + } + |> Seq.filter (fun x -> not (Set.contains x excludes)) + |> Seq.filter (fun x -> hashSet.Add x) + + files.GetEnumerator() + + member this.GetEnumerator() = + (this :> IEnumerable).GetEnumerator() + :> System.Collections.IEnumerator + +type ResolvedGlobbingPattern = + { + BaseDirectory: string + Includes: string list + Excludes: string list + Results: string list + } + + interface IGlobbingPattern with + member this.BaseDirectory = this.BaseDirectory + member this.Includes = this.Includes + member this.Excludes = this.Excludes + + interface IEnumerable with + member this.GetEnumerator() = + (this.Results :> IEnumerable).GetEnumerator() + + member this.GetEnumerator() = + (this :> IEnumerable).GetEnumerator() + :> System.Collections.IEnumerator + +[] +module GlobbingPatternExtensions = + type IGlobbingPattern with + + member internal this.Pattern = + match this with + | :? LazyGlobbingPattern as l -> l + | _ -> + { + BaseDirectory = this.BaseDirectory + Includes = this.Includes + Excludes = this.Excludes + } + + member this.Resolve() = + match this with + | :? ResolvedGlobbingPattern as res -> res :> IGlobbingPattern + | _ -> + let list = this |> Seq.toList + + { + BaseDirectory = this.BaseDirectory + Includes = this.Includes + Excludes = this.Excludes + Results = list + } + :> IGlobbingPattern + + /// Adds the given pattern to the file includes + member this.And pattern = + { this.Pattern with Includes = this.Includes @ [ pattern ] } + :> IGlobbingPattern + + /// Ignores files with the given pattern + member this.ButNot pattern = + { this.Pattern with Excludes = pattern :: this.Excludes } + :> IGlobbingPattern + + /// Sets a directory as BaseDirectory. + member this.SetBaseDirectory(dir: string) = + { this.Pattern with + BaseDirectory = dir.TrimEnd(Path.DirectorySeparatorChar) + } + :> IGlobbingPattern + + /// Checks if a particular file is matched + member this.IsMatch(path: string) = + let fullDir (pattern: string) = + if Path.IsPathRooted(pattern) then + pattern + else + System.IO.Path.Combine(this.BaseDirectory, pattern) + + let fullPath = Path.GetFullPath path + + let included = + this.Includes + |> List.exists (fun fileInclude -> + Glob.isMatch (fullDir fileInclude) fullPath + ) + + let excluded = + this.Excludes + |> List.exists (fun fileExclude -> + Glob.isMatch (fullDir fileExclude) fullPath + ) + + included && not excluded + +[] +module GlobbingPattern = + let private defaultBaseDir = Path.GetFullPath "." + + /// Include files + let create x = + { + BaseDirectory = defaultBaseDir + Includes = [ x ] + Excludes = [] + } + :> IGlobbingPattern + + /// Start an empty globbing pattern from the specified directory + let createFrom (dir: string) = + { + BaseDirectory = dir + Includes = [] + Excludes = [] + } + :> IGlobbingPattern + + /// Sets a directory as baseDirectory for fileIncludes. + let setBaseDir (dir: string) (fileIncludes: IGlobbingPattern) = + fileIncludes.SetBaseDirectory dir + + /// Get base include directories. + /// + /// Used to get a smaller set of directories from a globbing pattern. + let getBaseDirectoryIncludes (fileIncludes: IGlobbingPattern) = + let directoryIncludes = + fileIncludes.Includes + |> Seq.map (fun file -> + Glob.getRoot fileIncludes.BaseDirectory file + ) + + // remove subdirectories + directoryIncludes + |> Seq.filter (fun d -> + directoryIncludes + |> Seq.exists (fun p -> + d.StartsWith( + p + string Path.DirectorySeparatorChar, + StringComparison.Ordinal + ) + && p <> d + ) + |> not + ) + |> Seq.toList + +/// Contains operators to find and process files. +/// +/// ### Simple glob using as list +/// +/// let csProjectFiles = !! "src/*.csproj" +/// +/// for projectFile in csProjectFiles do +/// printf "F# ProjectFile: %s" projectFile +/// +/// ### Combine globs +/// +/// let projectFiles = +/// !! "src/*/*.*proj" +/// ++ "src/*/*.target" +/// -- "src/*/*.vbproj" +/// +/// for projectFile in projectFiles do +/// printf "ProjectFile: %s" projectFile +/// +module Operators = + /// Add Include operator + let inline (++) (x: IGlobbingPattern) pattern = x.And pattern + + /// Exclude operator + let inline (--) (x: IGlobbingPattern) pattern = x.ButNot pattern + + /// Includes a single pattern and scans the files - !! x = AllFilesMatching x + let inline (!!) x = GlobbingPattern.create x diff --git a/src/Fable.Compiler/Globbing.fsi b/src/Fable.Compiler/Globbing.fsi new file mode 100644 index 0000000000..4bae9ad036 --- /dev/null +++ b/src/Fable.Compiler/Globbing.fsi @@ -0,0 +1,112 @@ +module Fable.Compiler.Globbing + +open System.Collections.Generic +open System.IO + +/// Globbing support and operators +/// +/// Forked from `Fake.IO.FileSystem` +/// +/// This module contains a file pattern globbing implementation. +[] +module Glob = + open System + open System.Text.RegularExpressions + + val inline normalizePath: path: string -> string + + type private SearchOption = + | Directory of string + | Drive of string + | Recursive + | FilePattern of string + + val internal getRoot: baseDirectory: string -> pattern: string -> string + val internal search: baseDir: string -> originalInput: string -> string list + val internal compileGlobToRegex: pattern: string -> Regex + val isMatch: pattern: string -> path: string -> bool + +type IGlobbingPattern = + inherit IEnumerable + abstract BaseDirectory: string + abstract Includes: string list + abstract Excludes: string list + +type LazyGlobbingPattern = + { + BaseDirectory: string + Includes: string list + Excludes: string list + } + + interface IGlobbingPattern + interface IEnumerable + +type ResolvedGlobbingPattern = + { + BaseDirectory: string + Includes: string list + Excludes: string list + Results: string list + } + + interface IGlobbingPattern + interface IEnumerable + +[] +module GlobbingPatternExtensions = + type IGlobbingPattern with + + member internal Pattern: LazyGlobbingPattern + member Resolve: unit -> IGlobbingPattern + /// Adds the given pattern to the file includes + member And: pattern: string -> IGlobbingPattern + /// Ignores files with the given pattern + member ButNot: pattern: string -> IGlobbingPattern + /// Sets a directory as BaseDirectory. + member SetBaseDirectory: dir: string -> IGlobbingPattern + /// Checks if a particular file is matched + member IsMatch: path: string -> bool + +[] +module GlobbingPattern = + /// Include files + val create: x: string -> IGlobbingPattern + /// Start an empty globbing pattern from the specified directory + val createFrom: dir: string -> IGlobbingPattern + + /// Sets a directory as baseDirectory for fileIncludes. + val setBaseDir: + dir: string -> fileIncludes: IGlobbingPattern -> IGlobbingPattern + + /// Get base include directories. + /// + /// Used to get a smaller set of directories from a globbing pattern. + val getBaseDirectoryIncludes: fileIncludes: IGlobbingPattern -> string list + +/// Contains operators to find and process files. +/// +/// ### Simple glob using as list +/// +/// let csProjectFiles = !! "src/*.csproj" +/// +/// for projectFile in csProjectFiles do +/// printf "F# ProjectFile: %s" projectFile +/// +/// ### Combine globs +/// +/// let projectFiles = +/// !! "src/*/*.*proj" +/// ++ "src/*/*.target" +/// -- "src/*/*.vbproj" +/// +/// for projectFile in projectFiles do +/// printf "ProjectFile: %s" projectFile +/// +module Operators = + /// Add Include operator + val inline (++): x: IGlobbingPattern -> pattern: string -> IGlobbingPattern + /// Exclude operator + val inline (--): x: IGlobbingPattern -> pattern: string -> IGlobbingPattern + /// Includes a single pattern and scans the files - !! x = AllFilesMatching x + val inline (!!): x: string -> IGlobbingPattern diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs new file mode 100644 index 0000000000..799003610e --- /dev/null +++ b/src/Fable.Compiler/Library.fs @@ -0,0 +1,271 @@ +module Fable.Compiler.CodeServices + +open System +open System.IO +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.SourceCodeServices +open Fable +open Fable.Compiler.Util +open Fable.Transforms.State +open Fable.Transforms +open Fable.Compiler.ProjectCracker + +type BabelWriter + ( + com: Compiler, + pathResolver: PathResolver, + projectFile: string, + sourcePath: string, + targetPath: string + ) + = + // In imports *.ts extensions have to be converted to *.js extensions instead + // TODO: incomplete + let fileExt = ".js" + let sourceDir = Path.GetDirectoryName(sourcePath) + let targetDir = Path.GetDirectoryName(targetPath) + let memoryStream = new MemoryStream() + let streamWriter = new StreamWriter(memoryStream) + do streamWriter.NewLine <- "\n" + + // let mapGenerator = lazy (SourceMapSharp.SourceMapGenerator(?sourceRoot = cliArgs.SourceMapsRoot)) + + member x.ReadContentAsString() : Async = + async { + do! streamWriter.FlushAsync() |> Async.AwaitTask + memoryStream.Position <- 0L + let streamReader = new StreamReader(memoryStream) + return! (streamReader.ReadToEndAsync() |> Async.AwaitTask) + } + + interface Printer.Writer with + // Don't dispose the stream here because we need to access the memory stream to check if file has changed + member _.Dispose() = () + + member _.Write(str) = + streamWriter.WriteAsync(str) |> Async.AwaitTask + + member _.MakeImportPath(path) = + let projDir = Path.GetDirectoryName(projectFile) + + let path = + // TODO: Check precompiled out path for other languages too + match pathResolver.TryPrecompiledOutPath(sourceDir, path) with + | Some path -> Imports.getRelativePath sourceDir path + | None -> path + + // TODO: used to be cliArgs.outDir, could be wrong. + let path = + Imports.getImportPath + pathResolver + sourcePath + targetPath + projDir + (Some targetDir) + path + + if path.EndsWith(".fs", StringComparison.Ordinal) then + let isInFableModules = + Path.Combine(targetDir, path) |> Naming.isInFableModules + + File.changeExtensionButUseDefaultExtensionInFableModules + JavaScript + isInFableModules + path + fileExt + else + path + + member _.AddLog(msg, severity, ?range) = + com.AddLog( + msg, + severity, + ?range = range, + fileName = com.CurrentFile + ) + + member _.AddSourceMapping + ( + srcLine, + srcCol, + genLine, + genCol, + file, + displayName + ) + = + // + () +// if cliArgs.SourceMaps then +// let generated: SourceMapSharp.Util.MappingIndex = { line = genLine; column = genCol } +// let original: SourceMapSharp.Util.MappingIndex = { line = srcLine; column = srcCol } +// let targetPath = Path.normalizeFullPath targetPath +// let sourcePath = defaultArg file sourcePath |> Path.getRelativeFileOrDirPath false targetPath false +// mapGenerator.Force().AddMapping(generated, original, source=sourcePath, ?name=displayName) + +let compileFileToJs + (com: Compiler) + (pathResolver: PathResolver) + (outPath: string) + : Async + = + async { + let babel = + FSharp2Fable.Compiler.transformFile com + |> FableTransforms.transformFile com + |> Fable2Babel.Compiler.transformFile com + + use writer = + new BabelWriter( + com, + pathResolver, + com.ProjectFile, + com.CurrentFile, + outPath + ) + + do! BabelPrinter.run writer babel + let! output = writer.ReadContentAsString() + return output + } + +let compileProjectToJavaScript + (sourceReader: SourceReader) + (checker: InteractiveChecker) + (pathResolver: PathResolver) + (cliArgs: CliArgs) + (crackerResponse: CrackerResponse) + : Async> + = + async { + let! assemblies = checker.GetImportedAssemblies() + + let! checkProjectResult = + checker.ParseAndCheckProject( + cliArgs.ProjectFile, + crackerResponse.ProjectOptions.SourceFiles, + sourceReader + ) + + ignore checkProjectResult.Diagnostics + + let fableProj = + Project.From( + cliArgs.ProjectFile, + crackerResponse.ProjectOptions.SourceFiles, + checkProjectResult.AssemblyContents.ImplementationFiles, + assemblies + // ?precompiledInfo = + // (projCracked.PrecompiledInfo |> Option.map (fun i -> i :> _)), + // getPlugin = loadType projCracked.CliArgs + ) + + let opts = cliArgs.CompilerOptions + + let! compiledFiles = + crackerResponse.ProjectOptions.SourceFiles + |> Array.filter (fun filePath -> + not (filePath.EndsWith(".fsi", StringComparison.Ordinal)) + ) + |> Array.map (fun currentFile -> + async { + let fableLibDir = + Path.getRelativePath + currentFile + crackerResponse.FableLibDir + + let compiler: Compiler = + CompilerImpl( + currentFile, + fableProj, + opts, + fableLibDir, + crackerResponse.OutputType, + ?outDir = cliArgs.OutDir + ) + + let outputPath = Path.ChangeExtension(currentFile, ".js") + let! js = compileFileToJs compiler pathResolver outputPath + return currentFile, js + } + ) + |> Async.Parallel + + return Map.ofArray compiledFiles + } + +let compileFileToJavaScript + (sourceReader: SourceReader) + (checker: InteractiveChecker) + (pathResolver: PathResolver) + (cliArgs: CliArgs) + (crackerResponse: CrackerResponse) + (currentFile: string) + : Async> + = + async { + let! dependentFiles = + checker.GetDependentFiles( + currentFile, + crackerResponse.ProjectOptions.SourceFiles, + sourceReader + ) + + let lastFile = + if Array.isEmpty dependentFiles then + currentFile + else + Array.last dependentFiles + + let! assemblies = checker.GetImportedAssemblies() + + // Type-check the project up until the last dependent file. + let! checkProjectResult = + checker.ParseAndCheckProject( + cliArgs.ProjectFile, + crackerResponse.ProjectOptions.SourceFiles, + sourceReader, + lastFile = lastFile + ) + + let fableProj = + Project.From( + cliArgs.ProjectFile, + crackerResponse.ProjectOptions.SourceFiles, + checkProjectResult.AssemblyContents.ImplementationFiles, + assemblies + ) + + let opts = cliArgs.CompilerOptions + + let! compiledFiles = + dependentFiles + |> Array.filter (fun filePath -> + not (filePath.EndsWith(".fsi", StringComparison.Ordinal)) + ) + |> Array.map (fun currentFile -> + async { + let fableLibDir = + Path.getRelativePath + currentFile + crackerResponse.FableLibDir + + let compiler: Compiler = + CompilerImpl( + currentFile, + fableProj, + opts, + fableLibDir, + crackerResponse.OutputType, + ?outDir = cliArgs.OutDir + ) + + let outputPath = Path.ChangeExtension(currentFile, ".js") + let! js = compileFileToJs compiler pathResolver outputPath + return currentFile, js + } + ) + |> Async.Parallel + + return Map.ofArray compiledFiles + } diff --git a/src/Fable.Compiler/Library.fsi b/src/Fable.Compiler/Library.fsi new file mode 100644 index 0000000000..874d9ca4e3 --- /dev/null +++ b/src/Fable.Compiler/Library.fsi @@ -0,0 +1,27 @@ +module Fable.Compiler.CodeServices + +open FSharp.Compiler.SourceCodeServices +open Fable +open Fable.Compiler.Util +open Fable.Compiler.ProjectCracker + +/// Does a full type-check of the current project. +/// And compiles the implementation files to JavaScript. +val compileProjectToJavaScript: + sourceReader: SourceReader -> + checker: InteractiveChecker -> + pathResolver: PathResolver -> + cliArgs: CliArgs -> + crackerResponse: CrackerResponse -> + Async> + +/// Type-checks the project up until the last transitive dependent file. +/// Compile the current and the transitive dependent files to JavaScript. +val compileFileToJavaScript: + sourceReader: SourceReader -> + checker: InteractiveChecker -> + pathResolver: PathResolver -> + cliArgs: CliArgs -> + crackerResponse: CrackerResponse -> + currentFile: string -> + Async> diff --git a/src/Fable.Cli/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs similarity index 98% rename from src/Fable.Cli/ProjectCracker.fs rename to src/Fable.Compiler/ProjectCracker.fs index 188508da76..c5d0b7c67e 100644 --- a/src/Fable.Cli/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -1,6 +1,6 @@ -/// This module gets the F# compiler arguments from .fsproj as well as some +/// This module gets the F# compiler arguments from .fsproj as well as some /// Fable-specific tasks like tracking the sources of Fable Nuget packages -module Fable.Cli.ProjectCracker +module Fable.Compiler.ProjectCracker open System open System.Xml.Linq @@ -10,6 +10,7 @@ open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text open Fable open Fable.AST +open Fable.Compiler.Util open Globbing.Operators open Buildalyzer @@ -42,13 +43,13 @@ type CacheInfo = } static member GetPath(fableModulesDir: string, isDebug: bool) = - let suffix = - if isDebug then - "_debug" - else - "" - - IO.Path.Combine(fableModulesDir, $"project_cracked{suffix}.json") + IO.Path.Combine( + fableModulesDir, + $"""project_cracked{if isDebug then + "_debug" + else + ""}.json""" + ) member this.GetTimestamp() = CacheInfo.GetPath(this.FableModulesDir, this.FableOptions.DebugMode) @@ -145,7 +146,6 @@ type CrackerOptions(cliArgs: CliArgs) = static member GetFableModulesFromDir(baseDir: string) : string = IO.Path.Combine(baseDir, Naming.fableModules) |> Path.normalizePath - static member GetFableModulesFromProject ( projDir: string, @@ -173,18 +173,6 @@ type CrackerOptions(cliArgs: CliArgs) = fableModulesDir - member _.ResetFableModulesDir() = - if IO.Directory.Exists(fableModulesDir) then - IO.Directory.Delete(fableModulesDir, recursive = true) - - IO.Directory.CreateDirectory(fableModulesDir) |> ignore - - IO.File.WriteAllText( - IO.Path.Combine(fableModulesDir, ".gitignore"), - "**/*" - ) - - type CrackerResponse = { FableLibDir: string @@ -916,7 +904,6 @@ let getFableLibraryPath (opts: CrackerOptions) = ) let fableLibraryTarget = IO.Path.Combine(opts.FableModulesDir, libDir) - printfn $"Copying {fableLibrarySource} to {fableLibraryTarget}" // Always overwrite fable-library in case it has been updated, see #3208 copyDir false fableLibrarySource fableLibraryTarget Path.normalizeFullPath fableLibraryTarget @@ -1195,7 +1182,8 @@ let getFullProjectOpts (opts: CrackerOptions) = // The cache was considered outdated / invalid so it is better to make // make sure we have are in a clean state - opts.ResetFableModulesDir() + if IO.Directory.Exists(opts.FableModulesDir) then + IO.Directory.Delete(opts.FableModulesDir, true) let fableLibDir, pkgRefs = match opts.FableOptions.Language with diff --git a/src/Fable.Compiler/ProjectCracker.fsi b/src/Fable.Compiler/ProjectCracker.fsi new file mode 100644 index 0000000000..3ed6dbf62b --- /dev/null +++ b/src/Fable.Compiler/ProjectCracker.fsi @@ -0,0 +1,62 @@ +/// This module gets the F# compiler arguments from .fsproj as well as some +/// Fable-specific tasks like tracking the sources of Fable Nuget packages +module Fable.Compiler.ProjectCracker + +open FSharp.Compiler.CodeAnalysis +open Fable +open Fable.AST +open Fable.Compiler.Util + +type CacheInfo = + { + Version: string + FableOptions: CompilerOptions + ProjectPath: string + SourcePaths: string array + FSharpOptions: string array + References: string list + OutDir: string option + FableLibDir: string + FableModulesDir: string + OutputType: OutputType + TargetFramework: string + Exclude: string list + SourceMaps: bool + SourceMapsRoot: string option + } + +type CrackerOptions = + new: cliArgs: CliArgs -> CrackerOptions + member NoCache: bool + member CacheInfo: CacheInfo option + member FableModulesDir: string + member FableOptions: CompilerOptions + member FableLib: string option + member OutDir: string option + member Configuration: string + member Exclude: string list + member Replace: Map + member PrecompiledLib: string option + member NoRestore: bool + member ProjFile: string + member SourceMaps: bool + member SourceMapsRoot: string option + member BuildDll: normalizedDllPath: string -> unit + static member GetFableModulesFromDir: baseDir: string -> string + + static member GetFableModulesFromProject: + projDir: string * outDir: string option * noCache: bool -> string + +type CrackerResponse = + { + FableLibDir: string + FableModulesDir: string + References: string list + ProjectOptions: FSharpProjectOptions + OutputType: OutputType + TargetFramework: string + PrecompiledInfo: PrecompiledInfoImpl option + CanReuseCompiledFiles: bool + } + +val getFullProjectOpts: opts: CrackerOptions -> CrackerResponse diff --git a/src/Fable.Cli/Util.fs b/src/Fable.Compiler/Util.fs similarity index 99% rename from src/Fable.Cli/Util.fs rename to src/Fable.Compiler/Util.fs index 8ef93d9b6a..dd3a6a85c1 100644 --- a/src/Fable.Cli/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -1,4 +1,4 @@ -namespace Fable.Cli +module Fable.Compiler.Util #nowarn "3391" diff --git a/src/Fable.Cli/Util.fsi b/src/Fable.Compiler/Util.fsi similarity index 99% rename from src/Fable.Cli/Util.fsi rename to src/Fable.Compiler/Util.fsi index 716540fd7d..70123a5f09 100644 --- a/src/Fable.Cli/Util.fsi +++ b/src/Fable.Compiler/Util.fsi @@ -1,6 +1,4 @@ -namespace Fable.Cli - -#nowarn "3391" +module Fable.Compiler.Util open System diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 1b24b1bc6e..ca92b37e8b 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -5,6 +5,10 @@ netstandard2.0 $(OtherFlags) --nowarn:3536 + + + embedded + diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 5df165928e..9208ee7662 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -1079,7 +1079,7 @@ let rec transformDeclaration transformations (com: Compiler) file decl = } |> ClassDeclaration -let transformFile (com: Compiler) (file: File) = +let transformFile (com: Compiler) (file: Fable.AST.Fable.File) = let transformations = getTransformations com let newDecls = @@ -1087,4 +1087,4 @@ let transformFile (com: Compiler) (file: File) = (transformDeclaration transformations com file) file.Declarations - File(newDecls, usedRootNames = file.UsedNamesInRootScope) + Fable.AST.Fable.File(newDecls, usedRootNames = file.UsedNamesInRootScope) diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index f8b161cd71..4fe34e6f1b 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -62,6 +62,8 @@ type InlineExpr = type CompilerPlugins = { MemberDeclarationPlugins: Map } +type SourceReader = string -> int * Lazy + type Compiler = abstract LibraryDir: string abstract CurrentFile: string diff --git a/tests/Integration/Compiler/Util/Compiler.fs b/tests/Integration/Compiler/Util/Compiler.fs index bea40d0f67..26b1bc3453 100644 --- a/tests/Integration/Compiler/Util/Compiler.fs +++ b/tests/Integration/Compiler/Util/Compiler.fs @@ -1,13 +1,10 @@ namespace Fable.Tests.Compiler.Util open System -open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Diagnostics -open FSharp.Compiler.SourceCodeServices open Fable -open Fable.Cli open Fable.Cli.Main open Fable.Transforms.State +open Fable.Compiler.Util module Compiler =