Thursday, January 5, 2017

Run build tasks and targets via F# in VsCode

For developing the client side of a web app, I've switched from the ultra heavy Visual Studio 2015 to Visual Studio Code. It's written in JavaScript and runs pretty smoothly. It has hooks for kicking off a build, and tests.

I hooked up the build kick off to FAKE (an F# build engine) tasks (Via an .ionide file at the project root).

so I just hit Ctrl+Shift+P -> Fake -> and pick the build.fsx target I want to run from the menu (via keyboard, mouse if desired)

Things I have hooked up to be kicked off from Fake


  • Setup/install node (Via installing Chocolatey first)
    • install npm packages
  • Transpile Coffeescripts
  • Transpile Babel (React preset)
  • Transpile Sass
  • List packages installed to this project
  • Kill any running instances of the app
  • Clean the app
  • MsBuild the app
  • Kick off Mocha tests



[Fake]
linuxPrefix = "mono"
command = "build.cmd"
build = "build.fsx"
view raw .ionide hosted with ❤ by GitHub
// include Fake lib #r @"../packages/FAKE/tools/FakeLib.dll" open System open System.Diagnostics open System.IO open Fake // Properties let buildDir = "./bin/" let targetScript = let linqPadQueriesFolder = @"C:\projects\LinqPad\LinqPad\LINQPad Queries\" Path.Combine(linqPadQueriesFolder,@"WIP\runPmWeb.linq") let lprunPath = @"C:\ProgramData\LINQPad\Updates50\510\lprun.exe" let configLpRun (pi: ProcessStartInfo) = trace (sprintf "Target script:%s" targetScript) pi.FileName <- lprunPath pi.Arguments <- sprintf "\"%s\"" targetScript // Helpers let flip f y x = f x y let warn msg = trace (sprintf "WARNING: %s" msg) type System.String with static member Delimit delimiter (items:string seq) = String.Join(delimiter,items |> Array.ofSeq) module Sec = open System.Security.Principal let getIsAdmin() = WindowsIdentity.GetCurrent() |> WindowsPrincipal |> fun wp -> wp.IsInRole(WindowsBuiltInRole.Administrator) let requireAdmin () = let runningAsAdmin = getIsAdmin() if not runningAsAdmin then failwithf "Requested feature is not known to work without administrator permissions" module Proc = //let execCmd prog args timeout = let findCmd cmd = let processResult = ExecProcessAndReturnMessages (fun psi -> psi.FileName <- "where" psi.Arguments <- quoteIfNeeded cmd ) (TimeSpan.FromSeconds 2.) if processResult.OK then // require the result not be a directory let cmdPath = processResult.Messages |> Seq.filter (Directory.Exists >> not) |> Seq.filter (File.Exists) |> Seq.filter (fun x -> x.EndsWith ".bat" || x.EndsWith ".exe" || x.EndsWith ".cmd") |> Seq.tryHead if processResult.Messages.Count > 1 then warn (sprintf "found multiple items matching '%s'" cmd) trace (processResult.Messages |> String.Delimit ";") match cmdPath with | Some path -> trace (sprintf "found %s at %s" cmd path) Some path | None -> warn "where didn't return a valid file" None else None let runWithOutput cmd args timeOut = let cmd = // consider: what if the cmd is in the current dir? where may find one elsewhere first? if Path.IsPathRooted cmd then cmd else match findCmd cmd with | Some x -> x | None -> warn (sprintf "findCmd didn't find %s" cmd) cmd let result = ExecProcessAndReturnMessages (fun f -> //ExecProcessRedirected (fun f -> //f.FileName <- @"gulp" //f. Arguments <- "sass" // why did 'where' with no full path work, but this fails? f.FileName <- cmd f.Arguments <- args ) (TimeSpan.FromMinutes 1.0) result,cmd let showInExplorer path = Process.Start("explorer.exe",sprintf "/select, \"%s\"" path) // wrapper for fake built-in in case we want the entire process results, not just the exitcode let runElevated cmd args timeOut = let tempFilePath = System.IO.Path.GetTempFileName() // could also redirect error stream with 2> tempErrorFilePath // see also http://www.robvanderwoude.com/battech_redirection.php let resultCode = ExecProcessElevated "cmd" (sprintf "/c %s %s > %s" cmd args tempFilePath) timeOut trace "reading output results of runElevated" let outputResults = File.ReadAllLines tempFilePath File.Delete tempFilePath let processResult = ProcessResult.New resultCode (ResizeArray<_> outputResults) (ResizeArray<_>()) (String.Delimit "\r\n" outputResults) |> trace processResult type FindOrInstallResult = |Found |InstalledThenFound let findOrInstall cmd fInstall = match findCmd cmd with | Some x -> Some (x,Found) | None -> fInstall() findCmd cmd |> Option.map (fun x -> (x,InstalledThenFound)) module Node = let npmPath = lazy(Proc.findCmd "npm") // assumes the output is unimportant, just the result code let npmInstall args = let resultCode = let filename, useShell = match npmPath.Value with | Some x -> x, false // can't capture output with true | None -> "npm", true trace (sprintf "npm filename is %s" filename) ExecProcess (fun psi -> psi.FileName <- filename psi.Arguments <- "install" psi.UseShellExecute <- useShell ) (TimeSpan.FromMinutes 1.) resultCode // Targets Target "SetupNode" (fun _ -> // goal: install and setup everything required for any node dependencies this project has // including nodejs, gulp, node-sass // install Choco let chocoPath = let fInstall () = let resultCode = ExecProcessElevated "@powershell" """-NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" """ (TimeSpan.FromMinutes 3.) resultCode |> sprintf "choco install script returned %i" |> trace if resultCode <> 0 then failwithf "Task failed" // choco is installled, we think // probably won't work if it was just installed, the %path% variable given to/used by a process is immutable match Proc.findOrInstall "choco" fInstall with //| Some (x,Proc.FindOrInstallResult.Found) -> x | Some (x,_) -> x | None -> failwithf "choco was installed, in order for choco to be found or used, this has process has to be restarted" // choco install nodeJs let nodePath = let fInstall () = let results = Proc.runElevated "choco" "install nodejs -y" (TimeSpan.FromSeconds 3.) trace (sprintf "%A" results) match Proc.findOrInstall "node" fInstall with | Some (x,_) -> x | None -> failwithf "nodejs was installed, in order for node to be found or used, this process has to be restarted" // node should have installed npm // npm let npmPath = Proc.findCmd "npm" // install all packages that packages.json says this project needs let resultCode = let filename, useShell = match npmPath with | Some x -> x, false // can't capture output with true | None -> "npm", true trace (sprintf "npm filename is %s" filename) ExecProcess (fun psi -> psi.FileName <- filename psi.Arguments <- "install" psi.UseShellExecute <- useShell ) (TimeSpan.FromMinutes 1.) () ) // run node tests and whatever else Target "Test" (fun _ -> let result, _ = Proc.runWithOutput "npm" "test" (TimeSpan.FromSeconds 4.) result.Messages |> Seq.iter (printfn "test-msg:%s") if result.ExitCode <> 0 then result.Errors |> Seq.iter(printfn "test-err:%s") failwithf "Task failed: %i" result.ExitCode ) Target "Coffee" (fun _ -> let coffees = [ "test/test.coffee" ] let compileCoffee relPath = let result,_ = Proc.runWithOutput "node" (sprintf "node_modules/coffee-script/bin/coffee -b -m --no-header -c %s" relPath) (TimeSpan.FromSeconds 2.) if result.ExitCode <> 0 then failwithf "Task failed: %A" result coffees |> Seq.iter compileCoffee ) Target "Babel" (fun _ -> // run jsx compilation let babels = [ "Scripts/pm-era.jsx" "Scripts/pm-era-remitdetail.jsx" ] let babel relPath = let targetPath = let fullPath = Path.GetFullPath relPath Path.Combine(fullPath |> Path.GetDirectoryName, fullPath |> Path.GetFileNameWithoutExtension |> flip (+) ".react.js") let result,_ = Proc.runWithOutput "node" (sprintf "node_modules/babel-cli/bin/babel %s -o %s -s --presets react" relPath targetPath) (TimeSpan.FromSeconds 2.) if result.ExitCode <> 0 then result.Messages |> Seq.iter (printfn "babel-msg:%s") result.Errors |> Seq.iter(printfn "babel-err:%s") failwithf "Task failed: %i" result.ExitCode else result.Messages |> Seq.iter (printfn "babel-msg:%s") babels |> Seq.iter babel ) //node node_modules\coffee-script\bin\coffee -b -c test/test.coffee Target "Clean" (fun _ -> CleanDir buildDir let files = Directory.GetFiles buildDir |> Seq.length let directories = Directory.GetDirectories buildDir |> Seq.length printfn "cleaned directory had %i item(s) remaining after clean" (files + directories) ) Target "BuildApp" (fun _ -> let output = if isNullOrEmpty buildDir then "" else buildDir |> FullName |> trimSeparator let setParams defaults = { defaults with MSBuildParams.Targets= ["Build"] //Verbosity = Some(MSBuildVerbosity.Diagnostic) Properties = [ "Configuration", "Debug" "Platform", "AnyCPU" "DebugSymbols", "True" "OutputPath", buildDir "SolutionDir", ".." |> FullName ] } //https://github.com/fsharp/FAKE/blob/master/src/app/FakeLib/MSBuildHelper.fs build setParams "Pm.Web.fsproj" if isNotNullOrEmpty output then !!(buildDir @@ "/**/*.*") |> Seq.toList else [] |> Log "AppBuild-Output: " ) Target "StartApp" (fun _ -> let proc = new Process() configLpRun proc.StartInfo //proc.StartInfo.UseShellExecute <- false // not using ProcessHelper.Start we don't want fake killing it proc.Start() |> ignore trace (sprintf "started app with pid:%i" proc.Id) ) Target "Fire" (fun _ -> ProcessHelper.fireAndForget configLpRun ) Target "Run" (fun _ -> //C:\projects\LinqPad\LinqPad\LINQPad Queries\WIP\runPmWeb.linq //https://github.com/fsharp/FAKE/blob/master/src/app/FakeLib/ProcessHelper.fs asyncShellExec { Program = lprunPath WorkingDirectory = null CommandLine = sprintf "\"%s\"" targetScript Args = list.Empty } |> Async.RunSynchronously //Shell.Exec (,@"C:\projects\LinqPad\LinqPad\LINQPad Queries\",@"C:\projects\LinqPad\LinqPad\LINQPad Queries\WIP\") |> sprintf "script returned %i" |> trace ) Target "Stop" (fun _ -> killProcess "lprun" ) Target "NodeTasks" (fun _ -> trace "Node Tasks completed" ) Target "Sass" (fun _ -> let result,_ = Proc.runWithOutput "node-sass" "content/site.scss content/site.css" (TimeSpan.FromSeconds 4.) trace "finished attempting sass" printfn "sass:%A" result //result.Messages //|> Seq.iter (printfn "sass:%A") if result.ExitCode <> 0 then failwithf "Task failed" ) Target "NodeList" (fun _ -> let result = Proc.runWithOutput "npm" "list --depth=0" (TimeSpan.FromSeconds 2.0) printfn "npm list -g --depth=0:\r\n%A" result ) Target "Default" (fun _ -> trace "Hello World from FAKE" ) Target "AfterAll" (fun _ -> trace (sprintf "%A" DateTime.Now) ) // Dependencies // this should be NodeTasks depends on Sass, Coffee, and jsx compilation // ==> "Coffee" "Sass" ==> "NodeTasks" "Coffee" ==> "NodeTasks" "Stop" ==> "Run" For "NodeTasks" ["Sass";"Coffee";"Babel";"Test"] // "Stop" ==> "Fire" For "Fire" [ "Stop" ] For "StartApp" [ "Stop" ] For "StartApp" [ "Stop" ] For "Test" [ "Coffee" ] // default doesn't include starting the app "Stop" ==> "Clean" ==> "BuildApp" ==> "StartApp" ==> "Default" ==> "AfterAll" // make sure nodetasks happens before buildapp if nodetasks is executed "NodeTasks" ?=> "BuildApp" "Coffee" ?=> "Test" RunTargetOrDefault "Default"
view raw build.fsx hosted with ❤ by GitHub
{
"name": "pm.web",
"version": "1.0.0",
"description": "Registry additions have been made in order to provide you the best web development experience. See http://bloggemdano.blogspot.com/2013/11/adding-new-items-to-pure-f-aspnet.html for more information.",
"main": "index.js",
"dependencies": {
"requirejs": "^2.3.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-preset-react": "^6.16.0",
"coffee-script": "github:jashkenas/coffeescript",
"mocha": "^3.2.0"
},
"scripts": {
"wastest": "echo \"Error: no test specified\" && exit 1",
"test": "mocha"
},
"author": "",
"license": "ISC"
}
view raw package.json hosted with ❤ by GitHub
Version information: