Tuesday, May 9, 2017

Where has all the browser javascripting gone?

I guess it's time to admit I'm behind the times. I have little experience with many of the things I'm hearing the web-community talk about like it's something everyone is already using. Webpack, AMD, UMD, Flow, JsDoc, EsLint, Typings. I have some experience with node, but 99% of the code I write/have written is for the browser and blissfully ignorant of server-side JavaScript.

Individual parts of this I imagine I'd have no problem learning. It seems each one of them takes for granted that you already know or are willing to go learn the other system, and then not discuss how you would (as a beginner or intermediate to both systems) integrate them. Also, forget how you would use them on a path that doesn't perfectly match whatever path they choose for you to learn on, especially for any advanced usage.

Motivation

VsCode has a combination platter of lovely features/extensions to enable things like intellisense, auto-completion, and design-time error detection. However, what it seems I'm running into is they rely on using (non-AMD) module systems. I'm having code types/method thrashing in a live personal project (also a similar issue in a non-live non-personal project).

Solutions (from my Understanding/Attempts)

  • JsDoc
    • where it seems I have to redefine the same @typedef MyClass in each and every file rather than somehow referencing the source of an object's shape.
    • There doesn't appear to be anyway to get the annotations out of every file to centralize (which is of dubious benefit in many cases, unless it causes you to have to copy-paste your shape comments around to multiple files.
    • Some of it (no idea how much) isn't supported by VsCode
    • This is the documentation for a callback 
      • notice there is no example that doesn't involve using prototypes or resorting to classes?
      • I can find nothing that talks about a way to reference code in other files
  • Flow
    • all the documentation appears to say it always needs to be babel-ized to be in browser
    • running it, it complains like crazy about react 
      • I added it as a package, most likely to try to get flow or one of these other systems working
      • should the node react module be in the libs section of .flowconfig ?
      • when not in the libs section, and .*/node_modules/.* is in the ignore, it doesn't seem to care, still reports errors in node_modules/React/lib/ReactChildren.js
    • Whereas, I can use my jsx directly in browser via babel, I don't think I can do this with adding in flow-typing.
      • which means I don't have to figure out or take on learning Watcherify or something else to auto-magically detect changes and recompile on the fly.
    • aims at being an amazingly safe type system (vs TypeScript looking to strike a balance between safety and productivity) - see here
  • TypeScript 
    • I have no idea where to even start into this thing. It seems like a beast of changes to tackle to migrate anything into try it.
      • The examples demonstrate how to use either node or Visual Studio. I'm using (and loving) VsCode for all my JavaScript and some of my F#.
    • I suspect, were I to download the typings for react, my project (no TypeScript in it) would suddenly be able to design-time detect all kinds of problems.
    • I have no idea if I could write my own typings files for my non-JavaScript somewhere that would help anything. Furthermore, when I go look at something called DefinitelyTyped it looks like you have to do a lot of installations, and then elsewhere on the web, it's semi-deprecated since there is (or will be?) auto-discovery
  • React PropTypes
    • Everywhere it says move to an NPM package. no indications what to do if you don't develop your JavaScript in node. 
    • No sign, upon searching, of a CDN or other browser-capable file to help find method/object shape/type errors (which wouldn't happen until run time anyhow)
  • Fable - oh my beloved F# - produces so-so JavaScript, always bundled up in webpack that also doesn't seem to appreciate not having everything bundled into their own module system.
  • VsCode - oh so lovingly lightweight
    • doesn't support an unknown percentage of the JsDoc syntax
    • allows you to add ///<reference path="./x.extension"> but apparently only supports typescript files

This is at least partially a rant and venting

I have very little idea of what I'm talking about, having been stuck in a Wpf project for almost 2 years now. This is my way of categorizing and venting while trying to pick the flag back up. Also, a recounting of my experiences having been a cutting edge resource on near every coding-topic on every team I've ever been to now, feeling entirely lost trying to get a few features that seem almost entirely built-in to my IDE of choice for JavaScript. These are features I never even had a taste of before, and now it seems like I'm in instant withdrawals as soon as what I consider to be a near-critical feature is missing. (the features don't work if you aren't developing in node, aren't using webpack, are using AMD, etc..)

The topic

Very little of what I find that people recommend for usage explicitly appears to support JavaScript in browser. Node seems to have come up and choked out search results or the community or both from talking about in-browser or code that doesn't take a dependency in the entire codebase on a module system.

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: