diff --git a/.gitignore b/.gitignore
index acb4ec1..842e140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ nimcache/
karax/tools/karun
*.code-workspace
*.exe
+app.js
+app.html
diff --git a/karax.nimble b/karax.nimble
index bc157ab..a5b269c 100644
--- a/karax.nimble
+++ b/karax.nimble
@@ -8,7 +8,8 @@ license = "MIT"
# Dependencies
requires "nim >= 0.16.1"
-
+requires "ws"
+requires "dotenv"
skipDirs = @["examples", "experiments", "tests"]
bin = @["karax/tools/karun"]
diff --git a/karax/tools/karun.nim b/karax/tools/karun.nim
index 2b948b4..41410ca 100644
--- a/karax/tools/karun.nim
+++ b/karax/tools/karun.nim
@@ -1,21 +1,16 @@
-## Simple tool to quickly run Karax applications. Generates the HTML
-## required to run a Karax app and opens it in a browser.
-
-import os,
- strutils,
- parseopt,
- browsers,
- times,
- tables
-
+import os, strutils, browsers,times, tables
+import parseopt
+import threadpool
+import static_server
const
css = """
"""
- html = """
+
+const html = """
@@ -23,33 +18,79 @@ const
$1
$2
-
-
-
+
+
+
+$3
"""
+const websocket = """
+
+"""
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd
-proc build(name: string, rest: string, selectedCss: string, run: bool) =
+proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...")
- exec("nim js --out:" & name & ".js " & rest)
- let dest = name & ".html"
- writeFile(dest, html % [name, selectedCss])
- if run: openDefaultBrowser(dest)
+ let cmd = "nim js --out:" & "app" & ".js " & rest
+ if watch:
+ discard os.execShellCmd(cmd)
+ else:
+ exec cmd
+ let dest = "app" & ".html"
+ let script = if run and watch: websocket else: ""
+ writeFile(dest, html % ["app", selectedCss, script])
+ if run: openDefaultBrowser("http://localhost:8080")
+
+proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
+ var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
+ while true:
+ sleep(300)
+ for path in walkDirRec("."):
+ if ".git" in path:
+ continue
+
+ if files.hasKey(path):
+ if files[path] != getLastModificationTime(path):
+ echo("File changed: " & path)
+ build(rest,selectedCss, false, true)
+ files[path] = getLastModificationTime(path)
+ else:
+ if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
+ continue
+ files[path] = getLastModificationTime(path)
+
+proc serve(){.thread.} =
+ serveStatic()
proc main =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
- var selectedCss = ""
var watch = false
- var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
-
+ var selectedCss = ""
while true:
op.next()
case op.kind
@@ -59,8 +100,11 @@ proc main =
run = true
rest = rest.replace("--run ")
of "css":
- selectedCss = css
- rest = rest.replace("--css ")
+ if op.val != "":
+ selectedCss = readFile(op.val)
+ else:
+ selectedCss = css
+ rest = rest.substr(rest.find(" "))
else: discard
of cmdShortOption:
if op.key == "r":
@@ -73,25 +117,11 @@ proc main =
of cmdEnd: break
if file.len == 0: quit "filename expected"
- let name = file.splitFile.name
- build(name, rest, selectedCss, run)
+ if run:
+ spawn serve()
if watch:
- # TODO: launch http server
- while true:
- sleep(300)
- for path in walkDirRec("."):
- if ".git" in path:
- continue
- if files.hasKey(path):
- if files[path] != getLastModificationTime(path):
- echo("File changed: " & path)
- build(name, rest, selectedCss, run)
- files[path] = getLastModificationTime(path)
- else:
- files[path] = getLastModificationTime(path)
+ spawn watchBuild(file, selectedCss, rest)
+ build(rest,selectedCss, run, watch)
+ sync()
main()
-
-
-
-
diff --git a/karax/tools/karun.nims b/karax/tools/karun.nims
new file mode 100644
index 0000000..8983496
--- /dev/null
+++ b/karax/tools/karun.nims
@@ -0,0 +1 @@
+switch("threads", "on")
\ No newline at end of file
diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim
new file mode 100644
index 0000000..40375a9
--- /dev/null
+++ b/karax/tools/static_server.nim
@@ -0,0 +1,170 @@
+import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
+ logging, httpcore, asyncfile, options
+import asynchttpserver
+from cgi import decodeUrl
+import ws,tables,times
+import dotenv
+
+type
+ RawHeaders* = seq[tuple[key, val: string]]
+
+proc toStr(headers: Option[RawHeaders]): string =
+ return $newHttpHeaders(headers.get(@({:})))
+
+proc send(
+ request: Request, code: HttpCode, headers: Option[RawHeaders], body: string
+): Future[void] =
+ return request.respond(
+ code, body, newHttpHeaders(headers.get(@({:})))
+ )
+
+proc statusContent(request: Request, status: HttpCode, content: string,
+ headers: Option[RawHeaders]): Future[void] =
+ try:
+ result = send(request, status, headers, content)
+ when not defined(release):
+ logging.debug(" $1 $2" % [$status, toStr(headers)])
+ except:
+ logging.error("Could not send response: $1" % osErrorMsg(osLastError()))
+
+proc sendStaticIfExists(
+ req: Request, paths: seq[string]
+): Future[HttpCode] {.async.} =
+ result = Http200
+ let mimes = newMimetypes()
+ for p in paths:
+ if existsFile(p):
+
+ var fp = getFilePermissions(p)
+ if not fp.contains(fpOthersRead):
+ return Http403
+
+ let fileSize = getFileSize(p)
+ let ext = p.splitFile.ext
+ let mimetype = mimes.getMimetype(
+ if ext.len > 0: ext[1 .. ^1]
+ else: ""
+ )
+ if fileSize < 10_000_000: # 10 mb
+ var file = readFile(p)
+
+ var hashed = getMD5(file)
+
+ # If the user has a cached version of this file and it matches our
+ # version, let them use it
+ if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed:
+ await req.statusContent(Http304, "", none[RawHeaders]())
+ else:
+ await req.statusContent(Http200, file, some(@({
+ "Content-Type": mimetype,
+ "ETag": hashed
+ })))
+ else:
+ let headers = @({
+ "Content-Type": mimetype,
+ "Content-Length": $fileSize
+ })
+ await req.statusContent(Http200, "", some(headers))
+
+ var fileStream = newFutureStream[string]("sendStaticIfExists")
+ var file = openAsync(p, fmRead)
+ # Let `readToStream` write file data into fileStream in the
+ # background.
+ asyncCheck file.readToStream(fileStream)
+ # The `writeFromStream` proc will complete once all the data in the
+ # `bodyStream` has been written to the file.
+ while true:
+ let (hasValue, value) = await fileStream.read()
+ if hasValue:
+ await req.client.send(value)
+ else:
+ break
+ file.close()
+
+ return
+
+ # If we get to here then no match could be found.
+ return Http404
+
+
+proc handleFileRequest(
+ req: Request
+): Future[HttpCode] {.async.} =
+ # Find static file.
+ var reqPath = cgi.decodeUrl(req.url.path)
+ var publicUrl = getEnv("publicUrl")
+ var staticDir = getEnv("staticDir")
+ if not publicUrl.endsWith("/"):
+ publicUrl = publicUrl & "/"
+ reqPath = reqPath.substr(publicUrl.len)
+
+ let path = normalizedPath(
+ staticDir / reqPath
+ )
+
+ # Verify that this isn't outside our static dir.
+ var status = Http400
+ let pathDir = path.splitFile.dir / ""
+
+ if pathDir.startsWith(publicUrl):
+ if existsDir(path):
+ status = await sendStaticIfExists(
+ req,
+ @[path / "index.html", path / "index.htm"]
+ )
+ else:
+ status = await sendStaticIfExists(req, @[path])
+ return status
+
+proc handleWs(req: Request) {.async.} =
+ var ws = await newWebSocket(req)
+ await ws.send("Welcome to simple echo server")
+ var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
+ let watchedFiles = [absolutePath "app.js",absolutePath "app.html"]
+ for path in watchedFiles:
+ files[path] = getLastModificationTime(path)
+ while ws.readyState == Open:
+ await sleepAsync(500)
+ var changed = false
+ for path in watchedFiles:
+ if files[path] != getLastModificationTime(path):
+ changed = true
+ files[path] = getLastModificationTime(path)
+ if changed:
+ await ws.send("refresh")
+ changed = false
+
+
+proc serveStatic*() =
+ if fileExists( "static.env" ):
+ var env:DotEnv
+ env = initDotEnv(getCurrentDir(), "static.env")
+ env.overload()
+ else:
+ loadEnvFromString("""
+ staticDir="./src/assets/"
+ publicUrl="public"
+ """)
+
+ var server = newAsyncHttpServer()
+ proc cb(req: Request) {.gcsafe, async.} =
+ if req.url.path == "/ws":
+ await handleWs(req)
+ if req.url.path == "/":
+ await req.respond(Http200, readFile "app.html" )
+ elif req.url.path == "/app.js":
+ let file = absolutePath("app" & ".js")
+ if not file.fileExists:
+ logging.error("$1 not exists!" % file )
+ if fpUserRead notin os.getFilePermissions(file):
+ logging.error("Could not read $1!" % file )
+ await req.respond(Http200, readFile( file ))
+ else:
+ let status = await handleFileRequest(req)
+ if status != Http200:
+ await req.respond(status,"")
+
+ waitFor server.serve(Port(8080), cb)
+
+when isMainModule:
+ serveStatic()
\ No newline at end of file