From 8c7765480e33bc32a71b0ec2e2b1b7fc407ac9df Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Thu, 4 Jan 2018 22:41:03 -0700 Subject: [PATCH] all the goods --- bsconfig.json | 9 +-- package-lock.json | 22 +++++-- package.json | 9 ++- src/Async.re | 7 -- src/Async.rei | 13 ---- src/Commands.re | 8 +++ src/{native => }/Commands.rei | 19 +++++- src/Files.re | 8 +++ src/{native => }/Files.rei | 0 src/Utils.re | 7 ++ src/Utils.rei | 2 + src/js/Commands_js.re | 65 +++++++++++++++++++ src/js/Files_js.re | 44 +++++++++---- src/js/Utils_js.re | 2 + src/native/Async_native.re | 8 --- .../{BasicServer.re => BasicServer.re_x} | 0 .../{Commands.re => Commands_native.re} | 12 +++- src/native/{Files.re => Files_native.re} | 0 .../{StaticServer.re => StaticServer.re_x} | 14 +++- src/native/Utils_native.re | 2 + test/Test_js.re | 3 + test/Test_native.re | 31 +++++++++ test/{Test.re => Test_shared.re} | 36 ++-------- 23 files changed, 229 insertions(+), 92 deletions(-) delete mode 100644 src/Async.re delete mode 100644 src/Async.rei create mode 100644 src/Commands.re rename src/{native => }/Commands.rei (63%) create mode 100644 src/Files.re rename src/{native => }/Files.rei (100%) create mode 100644 src/Utils.re create mode 100644 src/Utils.rei create mode 100644 src/js/Commands_js.re create mode 100644 src/js/Utils_js.re delete mode 100644 src/native/Async_native.re rename src/native/{BasicServer.re => BasicServer.re_x} (100%) rename src/native/{Commands.re => Commands_native.re} (91%) rename src/native/{Files.re => Files_native.re} (100%) rename src/native/{StaticServer.re => StaticServer.re_x} (74%) create mode 100644 src/native/Utils_native.re create mode 100644 test/Test_js.re create mode 100644 test/Test_native.re rename test/{Test.re => Test_shared.re} (61%) diff --git a/bsconfig.json b/bsconfig.json index 85e5d4f..3d4ea84 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,9 +1,9 @@ { "name": "reason-cli-tools", - "namespace": true, + // "namespace": true, "bsc-flags": "-w -27 -g", "warnings": { - "number": "-40+6+7-26-27+32..39-28-44+45", + "number": "-40+6+7-26-27+32..39-28-44+45-102", "error": "+8", }, "sources": [ @@ -14,12 +14,13 @@ ]}, {"dir": "test", "type": "dev"}, ], + "ppx-flags": ["./node_modules/matchenv/matchenv"], "entries": [{ "backend": "native", - "main-module": "Test" + "main-module": "Test_native" }, { "backend": "js", - "main-module": "Test" + "main-module": "Test_js" }], "refmt": 3 } diff --git a/package-lock.json b/package-lock.json index fd21796..4da52ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,29 +14,39 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, "fd-slicer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "dev": true, "requires": { "pend": "1.2.0" } }, + "matchenv": { + "version": "github:bsansouci/matchenv#076d9d6b55c62721b289bdfa024e4f2c659e492b", + "requires": { + "bs-platform": "github:bsansouci/bsb-native#908e3a37aec1ec13f9207ab573ea6edfbd4d2b4d" + }, + "dependencies": { + "bs-platform": { + "version": "github:bsansouci/bsb-native#908e3a37aec1ec13f9207ab573ea6edfbd4d2b4d", + "requires": { + "yauzl": "2.9.1" + } + } + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" }, "yauzl": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", - "dev": true, "requires": { "buffer-crc32": "0.2.13", "fd-slicer": "1.0.1" diff --git a/package.json b/package.json index a58cf27..697ed23 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ "scripts": { "build": "bsb -make-world -backend native", "watch": "bsb -make-world -backend native -w", - "clean": "bsb -clean-world" + "clean": "bsb -clean-world", + "js": "bsb -make-world -backend js", + "js:watch": "bsb -make-world -backend js -w" }, - "// js": "bsb -make-world -backend js", - "// js:watch": "bsb -make-world -backend js -w", "devDependencies": { "bs-platform": "bsansouci/bsb-native#fast" + }, + "dependencies": { + "matchenv": "github:bsansouci/matchenv" } } diff --git a/src/Async.re b/src/Async.re deleted file mode 100644 index 1884818..0000000 --- a/src/Async.re +++ /dev/null @@ -1,7 +0,0 @@ - -/* #if BSB_BACKEND = "native" then */ -include Async_native; -/* #end -#if BSB_BACKEND = "js" then -include Async_js; -#end */ diff --git a/src/Async.rei b/src/Async.rei deleted file mode 100644 index 6d67794..0000000 --- a/src/Async.rei +++ /dev/null @@ -1,13 +0,0 @@ -type job; - -let kill: job => unit; - -/** - * Run an async command until it exits. - */ -let run: job => unit; - -/** - * Run multiple async commands in parallel until they all exit. - */ -let runAll: list(job) => unit; diff --git a/src/Commands.re b/src/Commands.re new file mode 100644 index 0000000..00f6111 --- /dev/null +++ b/src/Commands.re @@ -0,0 +1,8 @@ + +include [%matchenv + switch BSB_BACKEND { + | "bytecode" => Commands_native + | "native" => Commands_native + | "js" => Commands_js + } +]; diff --git a/src/native/Commands.rei b/src/Commands.rei similarity index 63% rename from src/native/Commands.rei rename to src/Commands.rei index 3c19fe8..8c8069e 100644 --- a/src/native/Commands.rei +++ b/src/Commands.rei @@ -1,11 +1,13 @@ +type job; + /* = (unit => unit, unit => unit); */ /** * Get the output of a command, in lines, and whether it succeeded. */ let execSync: (~cmd: string, ~onOut: string => unit=?, unit) => (list(string), bool); -let exec: (~cmd: string, ~onOut: string => unit) => Async.job; +let exec: (~cmd: string, ~onOut: string => unit) => job; /** * Returns a poll function, and a close function. Checks every `checkInterval` @@ -22,4 +24,17 @@ let keepAlive: ~onStart: unit => unit=?, ~checkInterval: float=?, unit - ) => Async.job; \ No newline at end of file + ) => job; + +let kill: job => unit; +let poll: job => unit; + +/** + * Run an async command until it exits. + */ +let run: job => unit; + +/** + * Run multiple async commands in parallel until they all exit. + */ +let runAll: list(job) => unit; \ No newline at end of file diff --git a/src/Files.re b/src/Files.re new file mode 100644 index 0000000..fa66472 --- /dev/null +++ b/src/Files.re @@ -0,0 +1,8 @@ + +include [%matchenv + switch BSB_BACKEND { + | "bytecode" => Files_native + | "native" => Files_native + | "js" => Files_js + } +]; diff --git a/src/native/Files.rei b/src/Files.rei similarity index 100% rename from src/native/Files.rei rename to src/Files.rei diff --git a/src/Utils.re b/src/Utils.re new file mode 100644 index 0000000..0a2b0f0 --- /dev/null +++ b/src/Utils.re @@ -0,0 +1,7 @@ +include [%matchenv + switch BSB_BACKEND { + | "bytecode" => Utils_native + | "native" => Utils_native + | "js" => Utils_js + } +]; diff --git a/src/Utils.rei b/src/Utils.rei new file mode 100644 index 0000000..a969714 --- /dev/null +++ b/src/Utils.rei @@ -0,0 +1,2 @@ + +let now: unit => float; \ No newline at end of file diff --git a/src/js/Commands_js.re b/src/js/Commands_js.re new file mode 100644 index 0000000..fddea3c --- /dev/null +++ b/src/js/Commands_js.re @@ -0,0 +1,65 @@ + + +type proc; + +type job = unit => unit; + +type buffer; +[@bs.module "child_process"] external spawnSync: + (~cmd: string, ~args: array(string), ~options: Js.t({. shell: Js.boolean})) => Js.t({. stdout: buffer, stderr: buffer, status: int}) = ""; + +[@bs.module "child_process"] external spawn: + (~cmd: string, ~args: array(string), ~options: Js.t({. shell: Js.boolean})) => proc = ""; + +[@bs.send] external on: (proc, string, unit => unit) => unit = ""; +[@bs.send] external kill: proc => unit = ""; +[@bs.send] external onData: (proc, string, buffer => unit) => unit = "on"; + +[@bs.send.pipe: buffer] external toString: (string) => string = ""; + +let trimLastNewline = text => if (Js.String.endsWith("\n", text)) { + Js.String.slice(~from=0, ~to_=Js.String.length(text) - 1, text) +} else { + text +}; + +let execSync = (~cmd, ~onOut=a => (), ()) => { + let res = spawnSync(~cmd, ~args=[||], ~options={"shell": Js.true_}); + (res##stdout |> toString("utf8") |> trimLastNewline |> Js.String.split("\n") |> Array.to_list, res##status == 0) +}; + +let exec = (~cmd, ~onOut) => { + let proc = spawn(~cmd, ~args=[||], ~options={"shell": Js.true_}); + () => () +}; + +let run = (job) => (); +let runAll = jobs => (); +let poll = (job) => (); + +let keepAlive = (~cmd, ~onOut=l => (), ~onErr=a => (), ~onStart=() => (), ~checkInterval=1., ()) => { + let save = ref(None); + let rec start = () => { + Js.log("starting"); + onStart(); + let proc = spawn(~cmd, ~args=[||], ~options={"shell": Js.true_}); + on(proc, "close", () => { + Js.log("close"); + Js.Global.setTimeout(() => { + save := Some(start()); + Js.log("AAA"); + }, int_of_float(checkInterval *. 1000.)) |> ignore; + }); + proc + }; + let kill = () => { + switch save^ { + | Some(p) => kill(p) + | None => () + } + }; + save := Some(start()); + kill +}; + +let kill = (job) => job(); \ No newline at end of file diff --git a/src/js/Files_js.re b/src/js/Files_js.re index d5bb049..4a9b85f 100644 --- a/src/js/Files_js.re +++ b/src/js/Files_js.re @@ -2,28 +2,30 @@ type stat; [@bs.module "fs"] external existsSync: string => bool = ""; +[@bs.module "fs"] external rmdirSync: string => unit = ""; [@bs.module "fs"] external readdirSync: string => array(string) = ""; -[@bs.module "fs"] external readFileSync: string => string = ""; -[@bs.module "fs"] external writeFileSync: string => string => string = ""; +[@bs.module "fs"] external readFileSync: string => string => string = ""; +[@bs.module "fs"] external writeFileSync: string => string => string => unit = ""; [@bs.module "fs"] external statSync: string => stat = ""; -[@bs.module "fs"] external mkdir: string => unit = ""; -[@bs.module "fs"] external unlink: string => unit = ""; +[@bs.module "fs"] external mkdirSync: string => unit = ""; +[@bs.module "fs"] external unlinkSync: string => unit = ""; [@bs.send] external isFile: stat => bool = ""; [@bs.send] external isDirectory: stat => bool = ""; [@bs.module "path"] external dirname: string => string = ""; [@bs.module "path"] external join: string => string => string = ""; -let removeDeep = path => { +let rec removeDeep = path => { if (existsSync(path)) { let stat = statSync(path); if (isDirectory(stat)) { Array.iter( name => removeDeep(join(path, name)), readdirSync(path) - ) + ); + rmdirSync(path); } else { - unlink(path) + unlinkSync(path) } } }; @@ -33,15 +35,31 @@ let isFile = path => try (isFile(statSync(path))) { | _ => false }; let isDirectory = path => try (isDirectory(statSync(path))) { | _ => false }; let rec mkdirp = path => { if (!exists(path)) { - mkdirp(dirname(path)) - mkdir(path); + mkdirp(dirname(path)); + mkdirSync(path); } }; let readDirectory = path => Array.to_list(readdirSync(path)); -let readFile = path => try (Some(readFileSync(path))) { | _ => None }; -let writeFile = (path, contents) => writeFileSync(path, contents, "utf8"); +let readFile = path => try (Some(readFileSync(path, "utf8"))) { | e => {Js.log(e); None} }; +let writeFile = (path, contents) => try {writeFileSync(path, contents, "utf8"); true} { | _ => false }; -let copy: (~source: string, ~dest: string) => bool; -let copyDeep: (~source: string, ~dest: string) => unit; +let copy = (~source, ~dest) => { + switch (readFile(source)) { + | None => false + | Some(text) => writeFile(dest, text) + } +}; + +let rec copyDeep = (~source, ~dest) => { + mkdirp(dirname(dest)); + if (isFile(source)) { + copy(~source, ~dest) |> ignore + } else if (isDirectory(source)) { + Array.iter( + name => copyDeep(~source=join(source, name), ~dest=join(dest, name)), + readdirSync(source) + ); + } +}; diff --git a/src/js/Utils_js.re b/src/js/Utils_js.re new file mode 100644 index 0000000..fb19f21 --- /dev/null +++ b/src/js/Utils_js.re @@ -0,0 +1,2 @@ + +let now = () => Js.Date.now() /. 1000.; diff --git a/src/native/Async_native.re b/src/native/Async_native.re deleted file mode 100644 index 3e57c39..0000000 --- a/src/native/Async_native.re +++ /dev/null @@ -1,8 +0,0 @@ - -type job = (unit => unit, unit => unit); - -let run = ((poll, close)) => while (true) poll(); - -let runAll = jobs => while (true) List.iter(f => ()); - -let kill = ((poll, close)) => close(); \ No newline at end of file diff --git a/src/native/BasicServer.re b/src/native/BasicServer.re_x similarity index 100% rename from src/native/BasicServer.re rename to src/native/BasicServer.re_x diff --git a/src/native/Commands.re b/src/native/Commands_native.re similarity index 91% rename from src/native/Commands.re rename to src/native/Commands_native.re index 6fadfe8..1305a18 100644 --- a/src/native/Commands.re +++ b/src/native/Commands_native.re @@ -1,3 +1,5 @@ +type job = (unit => unit, unit => unit); + /** * Get the output of a command, in lines. */ @@ -95,4 +97,12 @@ let keepAlive = (~cmd, ~onOut=line => (), ~onErr=line => (), ~onStart=() => (), Unix.kill(pid, 9) }; (poll, close) -}; \ No newline at end of file +}; + +let poll = ((poll, close)) => poll(); + +let run = ((poll, close)) => while (true) poll(); + +let runAll = jobs => while (true) List.iter(f => ()); + +let kill = ((poll, close)) => close(); \ No newline at end of file diff --git a/src/native/Files.re b/src/native/Files_native.re similarity index 100% rename from src/native/Files.re rename to src/native/Files_native.re diff --git a/src/native/StaticServer.re b/src/native/StaticServer.re_x similarity index 74% rename from src/native/StaticServer.re rename to src/native/StaticServer.re_x index c387942..7b03e7f 100644 --- a/src/native/StaticServer.re +++ b/src/native/StaticServer.re_x @@ -17,17 +17,25 @@ let ext = path => { List.nth(parts, List.length(parts) - 1) }; +let unwrap = (message, opt) => switch opt { | Some(x) => x | None => failwith(message)}; + open BasicServer.Response; + +let sendFile = (path, full_path) => switch (Files.readFile(full_path)) { +| Some(text) => Ok(mime_for_name(ext(path)), text) +| None => Bad(404, "File not found: " ++ path) +}; + let serveStatic = (full_path, path) => { switch (Unix.stat(full_path)) { | exception Unix.Unix_error(Unix.ENOENT, _, _) => Bad(404, "File not found: " ++ path) | stat => switch (stat.Unix.st_kind) { - | Unix.S_REG => Ok(mime_for_name(ext(path)), Files.readFile(full_path)) + | Unix.S_REG => sendFile(path, full_path) | Unix.S_DIR => { let index = Filename.concat(full_path, "index.html"); - if (isFile(index)) { - Ok("text/html", readFile(index)) + if (Files.isFile(index)) { + sendFile(path, index) } else { Ok("text/plain", "Directory") } diff --git a/src/native/Utils_native.re b/src/native/Utils_native.re new file mode 100644 index 0000000..b352a46 --- /dev/null +++ b/src/native/Utils_native.re @@ -0,0 +1,2 @@ + +let now = Unix.gettimeofday; diff --git a/test/Test_js.re b/test/Test_js.re new file mode 100644 index 0000000..8d621c2 --- /dev/null +++ b/test/Test_js.re @@ -0,0 +1,3 @@ +open Test_shared; + +report(); \ No newline at end of file diff --git a/test/Test_native.re b/test/Test_native.re new file mode 100644 index 0000000..acd9b76 --- /dev/null +++ b/test/Test_native.re @@ -0,0 +1,31 @@ + +open Test_shared; + +/* Pollable command */ +let job = Commands.exec(~cmd="yes", ~onOut=line => ()); +let t = Utils.now(); +while (Utils.now() -. t < 0.01) { + Commands.poll(job); +}; +Commands.kill(job); + +/* Keepalive */ +let starts = ref(0); +let job = Commands.keepAlive(~cmd="yes", ~checkInterval=0.01, ~onStart=() => starts := starts^ + 1, ()); +let t = Utils.now(); +while (Utils.now() -. t < 0.2) { + Commands.poll(job); +}; +Commands.kill(job); +ensure(starts^ == 1, "yes starts only once"); + +let starts = ref(0); +let job = Commands.keepAlive(~cmd="echo 'hi'", ~checkInterval=0.05, ~onStart=() => starts := starts^ + 1, ()); +let t = Utils.now(); +while (Utils.now() -. t < 2.) { + Commands.poll(job); +}; +Commands.kill(job); +ensure(starts^ > 1, "echo starts a bunch"); + +report(); \ No newline at end of file diff --git a/test/Test.re b/test/Test_shared.re similarity index 61% rename from test/Test.re rename to test/Test_shared.re index 577b8a8..0a5b50d 100644 --- a/test/Test.re +++ b/test/Test_shared.re @@ -22,42 +22,14 @@ let report = () => { }; /* Normal command */ -let (lines, succeeded) = Commands.readCommand(~cmd="echo 'hi'", ()); +let (lines, succeeded) = Commands.execSync(~cmd="echo 'hi'", ()); ensure(lines == ["hi"], "echo outputs hi"); ensure(succeeded, "echo exited successfully"); -/* Pollable command */ -let (poll, close) = Commands.pollableCommand(~cmd="yes", ~onOut=line => ()); -let t = Unix.gettimeofday(); -while (Unix.gettimeofday() -. t < 0.01) { - poll(); -}; -close(); - -/* Keepalive */ -let starts = ref(0); -let (poll, close) = Commands.keepAlive(~cmd="yes", ~checkInterval=0.01, ~onStart=() => starts := starts^ + 1, ()); -let t = Unix.gettimeofday(); -while (Unix.gettimeofday() -. t < 0.2) { - poll(); -}; -close(); -ensure(starts^ == 1, "yes starts only once"); - -let starts = ref(0); -let (poll, close) = Commands.keepAlive(~cmd="echo 'hi'", ~checkInterval=0.05, ~onStart=() => starts := starts^ + 1, ()); -let t = Unix.gettimeofday(); -while (Unix.gettimeofday() -. t < 0.2) { - poll(); -}; -close(); -ensure(starts^ > 1, "echo starts a bunch"); - ensure(Files.exists("./"), "directory exists"); ensure(Files.exists("./bsconfig.json"), "file exists"); ensure(!Files.exists("./noexist.json"), "nonexistant file"); -ensure(Files.readDirectory("./test") == ["Test.re"], "read dir"); let tmp = "./lib/tmp"; Files.mkdirp(tmp); @@ -67,6 +39,8 @@ let path = Filename.concat(tmp, "awesome.txt"); Files.writeFile(path, contents); ensure(Files.readFile(path) == Some(contents), "write & read a file"); +ensure(Files.readDirectory(tmp) == ["awesome.txt"], "read dir"); + let (/+) = Filename.concat; let deep = "a" /+ "b" /+ "c" /+ "more.thing"; let deepContents = "more stuff"; @@ -76,11 +50,9 @@ Files.writeFile(tmp /+ deep, deepContents); let tmpCopy = "tmpCopy"; Files.copyDeep(~source=tmp, ~dest=tmpCopy); -ensure(Files.readFile(tmpCopy /+ deep) == Some(deepContents), "write & read a file"); +ensure(Files.readFile(tmpCopy /+ deep) == Some(deepContents), "write & read a deep file"); Files.removeDeep(tmp); Files.removeDeep(tmpCopy); ensure(!Files.exists(tmp), "tmp removed"); ensure(!Files.exists(tmpCopy), "tmp copy removed"); - -report(); \ No newline at end of file