From 9665539fb1bfdfdd4ddde1dd10f223b6b3b102e4 Mon Sep 17 00:00:00 2001 From: George Lemon Date: Tue, 14 May 2024 21:42:11 +0300 Subject: [PATCH] wip universal api - server-side rendering powered by zmq Signed-off-by: George Lemon --- src/tim.nims | 1 + src/tim/app/compileCmd.nim | 33 ------ src/tim/app/jitCmd.nim | 32 ++++++ src/tim/app/liveCmd.nim | 23 ++++ src/tim/app/srcCmd.nim | 77 ++++++++++++++ src/tim/server/app.nim | 211 +++++++++++++++++++++++++++++++++++++ src/tim/server/config.nim | 19 ++++ src/tim/server/vm.nim | 43 ++++++++ 8 files changed, 406 insertions(+), 33 deletions(-) delete mode 100644 src/tim/app/compileCmd.nim create mode 100644 src/tim/app/jitCmd.nim create mode 100644 src/tim/app/liveCmd.nim create mode 100644 src/tim/app/srcCmd.nim create mode 100644 src/tim/server/app.nim create mode 100644 src/tim/server/config.nim create mode 100644 src/tim/server/vm.nim diff --git a/src/tim.nims b/src/tim.nims index 7be0861..33ed232 100755 --- a/src/tim.nims +++ b/src/tim.nims @@ -4,6 +4,7 @@ when defined napibuild: --define:napiOrWasm + --define:watchoutBrowserSync --noMain:on --passC:"-I/usr/include/node -I/usr/local/include/node" diff --git a/src/tim/app/compileCmd.nim b/src/tim/app/compileCmd.nim deleted file mode 100644 index 2c816ce..0000000 --- a/src/tim/app/compileCmd.nim +++ /dev/null @@ -1,33 +0,0 @@ -import std/[os, strutils] -import pkg/kapsis/[cli, runtime] -import ../engine/parser -import ../engine/logging -import ../engine/compilers/[html, nimc] - -proc cCommand*(v: Values) = - ## Transpiles a `.timl` file to a target source - let fpath = v.get("timl").getPath.path - let ext = v.get("ext").getStr - let pretty = v.has("pretty") - let p = parseSnippet(fpath, readFile(getCurrentDir() / fpath)) - if likely(not p.hasErrors): - if ext == "html": - let c = newCompiler(parser.getAst(p), pretty == false) - if likely(not c.hasErrors): - display c.getHtml().strip - else: - for err in c.logger.errors: - display err - displayInfo c.logger.filePath - quit(1) - elif ext == "nim": - let c = nimc.newCompiler(parser.getAst(p)) - display c.exportCode() - else: - displayError("Unknown target `" & ext & "`") - quit(1) - else: - for err in p.logger.errors: - display(err) - displayInfo p.logger.filePath - quit(1) diff --git a/src/tim/app/jitCmd.nim b/src/tim/app/jitCmd.nim new file mode 100644 index 0000000..dc9b46a --- /dev/null +++ b/src/tim/app/jitCmd.nim @@ -0,0 +1,32 @@ +import std/[osproc, os] +import pkg/flatty +import pkg/kapsis/[runtime, cli] + +import ../engine/[parser, ast] +import ../engine/compilers/nimc +import ../server/vm + +proc binCommand*(v: Values) = + ## Execute Just-in-Time compilation of the specifie + let + cachedPath = v.get("ast").getPath.path + cachedAst = readFile(cachedPath) + c = nimc.newCompiler(fromFlatty(cachedAst, Ast), true) + var + genFilepath = cachedPath.changeFileExt(".nim") + genFilepathTuple = genFilepath.splitFile() + # nim requires that module name starts with a letter + genFilepathTuple.name = "r_" & genFilepathTuple.name + genFilepath = genFilepathTuple.dir / genFilepathTuple.name & genFilepathTuple.ext + let dynlibPath = cachedPath.changeFileExt(".dylib") + writeFile(genFilepath, c.exportCode()) + # if not dynlibPath.fileExists: + let status = execCmdEx("nim c --mm:arc -d:danger --opt:speed --app:lib --noMain -o:" & dynlibPath & " " & genFilePath) + if status.exitCode > 0: + return # nim compilation error + removeFile(genFilepath) + var collection = DynamicTemplates() + let hashedName = cachedPath.splitFile.name + collection.load(hashedName) + echo collection.render(hashedName) + collection.unload(hashedName) \ No newline at end of file diff --git a/src/tim/app/liveCmd.nim b/src/tim/app/liveCmd.nim new file mode 100644 index 0000000..cdd7027 --- /dev/null +++ b/src/tim/app/liveCmd.nim @@ -0,0 +1,23 @@ +import std/[os, strutils] +import pkg/nyml +import pkg/kapsis/[cli, runtime] + +import ../server/[app, config] +import ../engine/meta + +proc runCommand*(v: Values) = + ## Run a new Universal Tim Engine microservice + ## in the background using the `tcp` socket. + ## + ## This feature is powered by ZeroMQ and makes Tim Engine + ## available from any programming language that implements `libzmq`. + ## + ## More details about ZeroMQ check [https://github.com/zeromq](https://github.com/zeromq) + let path = absolutePath(v.get("config").getPath.path) + let config = fromYaml(path.readFile, TimConfig) + var timEngine = newTim( + config.source, + config.output, + path.parentDir + ) + app.run(timEngine, config) \ No newline at end of file diff --git a/src/tim/app/srcCmd.nim b/src/tim/app/srcCmd.nim new file mode 100644 index 0000000..41a1596 --- /dev/null +++ b/src/tim/app/srcCmd.nim @@ -0,0 +1,77 @@ +import std/[os, strutils] +import pkg/kapsis/[cli, runtime] +import ../engine/parser +import ../engine/logging +import ../engine/compilers/[html, nimc] + +proc srcCommand*(v: Values) = + ## Transpiles a `.timl` file to a target source + let + fpath = v.get("timl").getStr + ext = v.get("ext").getStr + pretty = v.has("pretty") + # enableWatcher = v.has("w") + var + name: string + timlCode: string + if v.has"code": + timlCode = fpath + else: + name = fpath + timlCode = readFile(getCurrentDir() / fpath) + let p = parseSnippet(name, timlCode) + if likely(not p.hasErrors): + if ext == "html": + let c = html.newCompiler(parser.getAst(p), pretty == false) + if likely(not c.hasErrors): + display c.getHtml().strip + discard + else: + for err in c.logger.errors: + display err + displayInfo c.logger.filePath + quit(1) + elif ext == "nim": + let c = nimc.newCompiler(parser.getAst(p)) + display c.exportCode() + else: + displayError("Unknown target `" & ext & "`") + quit(1) + else: + for err in p.logger.errors: + display(err) + displayInfo p.logger.filePath + quit(1) + + +# import std/critbits + +# type +# ViewHandle* = proc(): string +# LayoutHandle* = proc(viewHtml: string): string +# ViewsTree* = CritBitTree[ViewHandle] +# LayoutsTree* = CritBitTree[LayoutHandle] + +# proc getIndex(): string = +# result = "view html" + +# var views = ViewsTree() +# views["index"] = getIndex + +# proc getBaseLayout(viewHtml: string): string = +# result = "start layout" +# add result, viewHtml +# add result, "end layout" + +# var layouts = LayoutsTree() +# layouts["base"] = getBaseLayout + +# template render*(viewName: string, layoutName = "base"): untyped = +# if likely(views.hasKey(viewName)): +# let viewHtml = views[viewName]() +# if likely(layouts.hasKey(layoutName)): +# layouts[layoutName](viewHtml) +# else: "" +# else: "" + +# echo render("index") \ No newline at end of file diff --git a/src/tim/server/app.nim b/src/tim/server/app.nim new file mode 100644 index 0000000..eb5d097 --- /dev/null +++ b/src/tim/server/app.nim @@ -0,0 +1,211 @@ +import std/[os, strutils, sequtils, json, critbits] +import pkg/[zmq, watchout, jsony] +import pkg/kapsis/[cli] + +import ./config +import ../engine/[meta, parser, logging] +import ../engine/compilers/[html, nimc] + +type + CacheTable = CritBitTree[string] + +var Cache = CacheTable() +const + address = "tcp://127.0.0.1:5559" + DOCKTYPE = "" + defaultLayout = "base" + +template displayErrors(l: Logger) = + for err in l.errors: + display(err) + display(l.filePath) + +proc transpileCode(engine: TimEngine, tpl: TimTemplate, + config: TimConfig, refreshAst = false) = + ## Transpile `tpl` TimTemplate to a specific target source + var p: Parser = engine.newParser(tpl, refreshAst = refreshAst) + if likely(not p.hasError): + case config.target + of tsHtml: + # When `HTML` is the preferred target source + # Tim Engine will run as a microservice app in background + # powered by Zero MQ. The pre-compiled templates are stored + # in a Cache table for when rendering is needed. + # echo engine.getTargetSourcePath(tpl, config.output, $config.target) + if tpl.jitEnabled(): + # if marked as JIT will save the produced + # binary AST on disk for runtime computation + engine.writeAst(tpl, parser.getAst(p)) + else: + # otherwise, compiles AST to static HTML + var c = html.newCompiler(engine, parser.getAst(p), tpl, + engine.isMinified, engine.getIndentSize) + if likely(not c.hasError): + case tpl.getType: + of ttView: + engine.writeHtml(tpl, c.getHtml) + of ttLayout: + engine.writeHtml(tpl, c.getHead) + else: discard + else: + c.logger.displayErrors() + # Cache[tpl.getHash()] = c.getHtml().strip + of tsNim: + let c = nimc.newCompiler(parser.getAst(p)) + writeFile(engine.getTargetSourcePath(tpl, config.output, $config.target), c.exportCode()) + else: discard + else: p.logger.displayErrors() + +proc resolveDependants(engine: TimEngine, + deps: seq[string], config: TimConfig) = + for path in deps: + let tpl = engine.getTemplateByPath(path) + case tpl.getType + of ttPartial: + echo tpl.getDeps.toSeq + engine.resolveDependants(tpl.getDeps.toSeq, config) + else: + engine.transpileCode(tpl, config, true) + +proc precompile(engine: TimEngine, config: TimConfig, globals: JsonNode) = + ## Pre-compiles available templates + engine.setGlobalData(globals) + proc notify(label, fname: string) = + echo label + echo indent(fname & "\n", 3) + + # Callback `onFound` + proc onFound(file: watchout.File) = + # Runs when detecting a new template. + let tpl: TimTemplate = + engine.getTemplateByPath(file.getPath()) + case tpl.getType + of ttView, ttLayout: + engine.transpileCode(tpl, config) + else: discard + + # Callback `onChange` + proc onChange(file: watchout.File) = + # Runs when detecting changes + let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) + notify("✨ Changes detected", file.getName()) + case tpl.getType() + of ttView, ttLayout: + engine.transpileCode(tpl, config) + else: + engine.resolveDependants(tpl.getDeps.toSeq, config) + + # Callback `onDelete` + proc onDelete(file: watchout.File) = + # Runs when deleting a file + notify("✨ Deleted", file.getName()) + engine.clearTemplateByPath(file.getPath()) + + var watcher = + newWatchout( + @[engine.getSourcePath() / "*"], + onChange, onFound, onDelete, + recursive = true, + ext = @["timl"], delay = config.sync.delay, + browserSync = + WatchoutBrowserSync( + port: config.sync.port, + delay: config.sync.delay + ) + ) + # watch for file changes in a separate thread + watcher.start(config.target != tsHtml) + +proc jitCompiler(engine: TimEngine, + tpl: TimTemplate, data: JsonNode): HtmlCompiler = + ## Compiles `tpl` AST at runtime + html.newCompiler( + engine, + engine.readAst(tpl), + tpl, + engine.isMinified, + engine.getIndentSize, + data + ) + +template layoutWrapper(getViewBlock) {.dirty.} = + result = DOCKTYPE + var layoutTail: string + var hasError: bool + if not layout.jitEnabled: + # when requested layout is pre-rendered + # will use the static HTML version from disk + add result, layout.getHtml() + getViewBlock + layoutTail = layout.getTail() + else: + var jitLayout = engine.jitCompiler(layout, data) + if likely(not jitLayout.hasError): + add result, jitLayout.getHead() + getViewBlock + layoutTail = jitLayout.getTail() + else: + hasError = true + jitLayout.logger.displayErrors() + add result, layoutTail + +proc render(engine: TimEngine, viewName, layoutName: string, local = newJObject()): string = + # Renders a `viewName` + if likely(engine.hasView(viewName)): + var + view: TimTemplate = engine.getView(viewName) + data: JsonNode = newJObject() + data["local"] = local + if likely(engine.hasLayout(layoutName)): + var layout: TimTemplate = engine.getLayout(layoutName) + if not view.jitEnabled: + # render a pre-compiled HTML + layoutWrapper: + add result, indent(view.getHtml(), layout.getViewIndent) + else: + # compile and render template at runtime + layoutWrapper: + var jitView = engine.jitCompiler(view, data) + if likely(not jitView.hasError): + add result, indent(jitView.getHtml(), layout.getViewIndent) + else: + jitView.logger.displayErrors() + hasError = true + else: + raise newException(TimError, "View not found") + +proc run*(engine: var TimEngine, config: TimConfig) = + config.output = normalizedPath(engine.getBasePath / config.output) + case config.target + of tsHtml: + display("Tim Engine is running at " & address) + var rep = listen(address, mode = REP) + var hasGlobalStorage: bool + defer: rep.close() + while true: + let req = rep.receiveAll() + try: + let command = req[0] + case command + of "render": + let local = req[3].fromJson + let output = engine.render(req[1], req[2], local) + rep.send(output) + of "global.storage": # runs once + if not hasGlobalStorage: + let globals = req[1].fromJson + engine.precompile(config, globals) + hasGlobalStorage = true + rep.send("") + else: + rep.send("") + else: discard # unknown command error ? + except TimError as e: + rep.send(e.msg) + sleep(10) + else: + discard existsOrCreateDir(config.output / "views") + discard existsOrCreateDir(config.output / "layouts") + discard existsOrCreateDir(config.output / "partials") + display("Tim Engine is running Source-to-Source") + engine.precompile(config, newJObject()) \ No newline at end of file diff --git a/src/tim/server/config.nim b/src/tim/server/config.nim new file mode 100644 index 0000000..2401e56 --- /dev/null +++ b/src/tim/server/config.nim @@ -0,0 +1,19 @@ +from std/net import Port, `$` +export `$` + +type + TargetSource* = enum + tsNim = "nim" + tsJS = "js" + tsHtml = "html" + tsRuby = "rb" + tsPython = "py" + + BrowserSync* = ref object + port*: Port + delay*: uint # ms + + TimConfig* = ref object + target*: TargetSource + source*, output*: string + sync*: BrowserSync \ No newline at end of file diff --git a/src/tim/server/vm.nim b/src/tim/server/vm.nim new file mode 100644 index 0000000..1b08c57 --- /dev/null +++ b/src/tim/server/vm.nim @@ -0,0 +1,43 @@ +# A super fast template engine for cool kids +# +# (c) 2023 George Lemon | LGPL License +# Made by Humans from OpenPeeps +# https://github.com/openpeeps/tim + +## This module implements a high-performance Just-in-Time +import std/[tables, dynlib, json] +type + DynamicTemplate = object + name: string + lib: LibHandle + function: Renderer + Renderer = proc(app, this: JsonNode = newJObject()): string {.gcsafe, stdcall.} + DynamicTemplates* = ref object + templates: OrderedTableRef[string, DynamicTemplate] = newOrderedTable[string, Dynamictemplate]() + +when defined macosx: + const ext = ".dylib" +elif windows: + const ext = ".dll" +else: + const ext = ".so" + +proc load*(collection: DynamicTemplates, t: string) = + ## Load a Dynamic template + var tpl = DynamicTemplate(lib: loadLib(t & ext)) + tpl.function = cast[Renderer](tpl.lib.symAddr("renderTemplate")) + collection.templates[t] = tpl + +proc reload*(collection: DynamicTemplates, t: string) = + ## Reload a Dynamic template + discard + +proc unload*(collection: DynamicTemplates, t: string) = + ## Unload a Dynamic template + dynlib.unloadLib(collection.templates[t].lib) + reset(collection.templates[t]) + collection.templates.del(t) + +proc render*(collection: DynamicTemplates, t: string): string = + if likely(collection.templates.hasKey(t)): + return collection.templates[t].function(this = %*{"x": "ola!"})