summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/lib.rst3
-rw-r--r--lib/js/jsffi.nim436
-rw-r--r--tests/js/tjsffi.nim267
-rw-r--r--web/website.ini2
4 files changed, 707 insertions, 1 deletions
diff --git a/doc/lib.rst b/doc/lib.rst
index 8bb602b78..6b498e696 100644
--- a/doc/lib.rst
+++ b/doc/lib.rst
@@ -415,6 +415,9 @@ Modules for JS backend
 * `dom <dom.html>`_
   Declaration of the Document Object Model for the JS backend.
 
+* `jsffi <jsffi.html>`_
+  Types and macros for easier interaction with JavaScript.
+
 
 Deprecated modules
 ------------------
diff --git a/lib/js/jsffi.nim b/lib/js/jsffi.nim
new file mode 100644
index 000000000..646b79fe5
--- /dev/null
+++ b/lib/js/jsffi.nim
@@ -0,0 +1,436 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2017 Nim Authors
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## This Module implements types and macros to facilitate the wrapping of, and
+## interaction with JavaScript libraries. Using the provided types ``JsObject``
+## and ``JsAssoc`` together with the provided macros allows for smoother
+## interfacing with JavaScript, allowing for example quick and easy imports of
+## JavaScript variables:
+##
+## .. code-block:: nim
+##
+##  # Here, we are using jQuery for just a few calls and do not want to wrap the
+##  # whole library:
+##
+##  # import the document object and the console
+##  var document {. importc, nodecl .}: JsObject
+##  var console {. importc, nodecl .}: JsObject
+##  # import the "$" function
+##  proc jq(selector: JsObject): JsObject {. importcpp: "$(#)" .}
+##
+##  # Use jQuery to make the following code run, after the document is ready.
+##  # This uses an experimental ``.()`` operator for ``JsObject``, to emit
+##  # JavaScript calls, when no corresponding proc exists for ``JsObject``.
+##  proc main =
+##    jq(document).ready(proc() =
+##      console.log("Hello JavaScript!")
+##    )
+##
+
+when not defined(js) and not defined(nimdoc) and not defined(nimsuggest):
+  {.fatal: "Module jsFFI is designed to be used with the JavaScript backend.".}
+
+import macros, tables
+
+const
+  setImpl = "#[#] = #"
+  getImpl = "#[#]"
+
+var
+  mangledNames {. compileTime .} = initTable[string, string]()
+  nameCounter {. compileTime .} = 0
+
+proc validJsName(name: string): bool =
+  result = true
+  const reservedWords = ["break", "case", "catch", "class", "const", "continue",
+    "debugger", "default", "delete", "do", "else", "export", "extends",
+    "finally", "for", "function", "if", "import", "in", "instanceof", "new",
+    "return", "super", "switch", "this", "throw", "try", "typeof", "var",
+    "void", "while", "with", "yield", "enum", "implements", "interface",
+    "let", "package", "private", "protected", "public", "static", "await",
+    "abstract", "boolean", "byte", "char", "double", "final", "float", "goto",
+    "int", "long", "native", "short", "synchronized", "throws", "transient",
+    "volatile", "null", "true", "false"]
+  case name
+  of reservedWords: return false
+  else: discard
+  if name[0] notin {'A'..'Z','a'..'z','_','$'}: return false
+  for chr in name:
+    if chr notin {'A'..'Z','a'..'z','_','$','0'..'9'}:
+      return false
+
+template mangleJsName(name: cstring): cstring =
+  inc nameCounter
+  "mangledName" & $nameCounter
+
+type
+  JsRoot* = ref object of RootObj
+    ## Root type of both JsObject and JsAssoc
+  JsObject* = ref object of JsRoot
+    ## Dynamically typed wrapper around a JavaScript object.
+  JsAssoc*[K, V] = ref object of JsRoot
+    ## Statically typed wrapper around a JavaScript object.
+  NotString = concept c
+    c isnot string
+
+# New
+proc newJsObject*: JsObject {. importcpp: "{@}" .}
+  ## Creates a new empty JsObject
+proc newJsAssoc*[K, V]: JsAssoc[K, V] {. importcpp: "{@}" .}
+  ## Creates a new empty JsAssoc with key type `K` and value type `V`.
+
+# Checks
+proc hasOwnProperty*(x: JsObject, prop: cstring): bool
+  {. importcpp: "#.hasOwnProperty(#)" .}
+  ## Checks, whether `x` has a property of name `prop`.
+
+proc jsTypeOf*(x: JsObject): cstring {. importcpp: "typeof(#)" .}
+  ## Returns the name of the JsObject's JavaScript type as a cstring.
+
+# Conversion to and from JsObject
+proc to*(x: JsObject, T: typedesc): T {. importcpp: "(#)" .}
+  ## Converts a JsObject `x` to type `T`.
+proc toJs*[T](val: T): JsObject {. importcpp: "(#)" .}
+  ## Converts a value of any type to type JsObject
+
+proc `[]`*(obj: JsObject, field: cstring): JsObject {. importcpp: getImpl .}
+  ## Return the value of a property of name `field` from a JsObject `obj`.
+
+proc `[]=`*[T](obj: JsObject, field: cstring, val: T) {. importcpp: setImpl .}
+  ## Set the value of a property of name `field` in a JsObject `obj` to `v`.
+
+proc `[]`*[K: NotString, V](obj: JsAssoc[K, V], field: K): V
+  {. importcpp: getImpl .}
+  ## Return the value of a property of name `field` from a JsAssoc `obj`.
+
+proc `[]`*[V](obj: JsAssoc[string, V], field: cstring): V
+  {. importcpp: getImpl .}
+  ## Return the value of a property of name `field` from a JsAssoc `obj`.
+
+proc `[]=`*[K: NotString, V](obj: JsAssoc[K, V], field: K, val: V)
+  {. importcpp: setImpl .}
+  ## Set the value of a property of name `field` in a JsAssoc `obj` to `v`.
+
+proc `[]=`*[V](obj: JsAssoc[string, V], field: cstring, val: V)
+  {. importcpp: setImpl .}
+  ## Set the value of a property of name `field` in a JsAssoc `obj` to `v`.
+
+proc `==`*(x, y: JsRoot): bool {. importcpp: "(# === #)" .}
+  ## Compare two JsObjects or JsAssocs. Be careful though, as this is comparison
+  ## like in JavaScript, so if your JsObjects are in fact JavaScript Objects,
+  ## and not strings or numbers, this is a *comparison of references*.
+
+{. experimental .}
+macro `.`*(obj: JsObject, field: static[cstring]): JsObject =
+  ## Experimental dot accessor (get) for type JsObject.
+  ## Returns the value of a property of name `field` from a JsObject `x`.
+  ##
+  ## Example:
+  ##
+  ## .. code-block:: nim
+  ##
+  ##  let obj = newJsObject()
+  ##  obj.a = 20
+  ##  console.log(obj.a) # puts 20 onto the console.
+  if validJsName($field):
+    let importString = "#." & $field
+    result = quote do:
+      proc helper(o: JsObject): JsObject
+        {. importcpp: `importString`, gensym .}
+      helper(`obj`)
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = $mangleJsName(field)
+    let importString = "#." & mangledNames[$field]
+    result = quote do:
+      proc helper(o: JsObject): JsObject
+        {. importcpp: `importString`, gensym .}
+      helper(`obj`)
+
+macro `.=`*(obj: JsObject, field: static[cstring], value: untyped): untyped =
+  ## Experimental dot accessor (set) for type JsObject.
+  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
+  if validJsName($field):
+    let importString = "#." & $field & " = #"
+    result = quote do:
+      proc helper(o: JsObject, v: auto)
+        {. importcpp: `importString`, gensym .}
+      helper(`obj`, `value`)
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = $mangleJsName(field)
+    let importString = "#." & mangledNames[$field] & " = #"
+    result = quote do:
+      proc helper(o: JsObject, v: auto)
+        {. importcpp: `importString`, gensym .}
+      helper(`obj`, `value`)
+
+macro `.()`*(obj: JsObject, field: static[cstring],
+    args: varargs[JsObject, toJs]): JsObject =
+  ## Experimental "method call" operator for type JsObject.
+  ## Takes the name of a method of the JavaScript object (`field`) and calls
+  ## it with `args` as arguments, returning a JsObject (which may be discarded,
+  ## and may be `undefined`, if the method does not return anything,
+  ## so be careful when using this.)
+  ##
+  ## Example:
+  ##
+  ## .. code-block:: nim
+  ##
+  ##  # Let's get back to the console example:
+  ##  var console {. importc, nodecl .}: JsObject
+  ##  let res = console.log("I return undefined!")
+  ##  console.log(res) # This prints undefined, as console.log always returns
+  ##                   # undefined. Thus one has to be careful, when using
+  ##                   # JsObject calls.
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field & "(@)"
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = $mangleJsName(field)
+    importString = "#." & mangledNames[$field] & "(@)"
+  result = quote do:
+    proc helper(o: JsObject): JsObject
+      {. importcpp: `importString`, gensym .}
+    helper(`obj`)
+  for idx in 0 ..< args.len:
+    let paramName = newIdentNode(!("param" & $idx))
+    result[0][3].add newIdentDefs(paramName, newIdentNode(!"JsObject"))
+    result[1].add args[idx].copyNimTree
+
+macro `.`*[K: string | cstring, V](obj: JsAssoc[K, V],
+  field: static[cstring]): V =
+  ## Experimental dot accessor (get) for type JsAssoc.
+  ## Returns the value of a property of name `field` from a JsObject `x`.
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = $mangleJsName(field)
+    importString = "#." & mangledNames[$field]
+  result = quote do:
+    proc helper(o: type(`obj`)): `obj`.V
+      {. importcpp: `importString`, gensym .}
+    helper(`obj`)
+
+macro `.=`*[K: string | cstring, V](obj: JsAssoc[K, V],
+  field: static[cstring], value: V): untyped =
+  ## Experimental dot accessor (set) for type JsAssoc.
+  ## Sets the value of a property of name `field` in a JsObject `x` to `value`.
+  var importString: string
+  if validJsName($field):
+    importString = "#." & $field & " = #"
+  else:
+    if not mangledNames.hasKey($field):
+      mangledNames[$field] = $mangleJsName(field)
+    importString = "#." & mangledNames[$field] & " = #"
+  result = quote do:
+    proc helper(o: type(`obj`), v: `obj`.V)
+      {. importcpp: `importString`, gensym .}
+    helper(`obj`, `value`)
+
+macro `.()`*[K: string | cstring, V: proc](obj: JsAssoc[K, V],
+  field: static[cstring], args: varargs[untyped]): auto =
+  ## Experimental "method call" operator for type JsAssoc.
+  ## Takes the name of a method of the JavaScript object (`field`) and calls
+  ## it with `args` as arguments. Here, everything is typechecked, so you do not
+  ## have to worry about `undefined` return values.
+  let dotOp = bindSym"."
+  result = quote do:
+    (`dotOp`(`obj`, `field`))()
+  for elem in args:
+    result[0].add elem
+
+# Iterators:
+
+iterator pairs*(obj: JsObject): (cstring, JsObject) =
+  ## Yields tuples of type ``(cstring, JsObject)``, with the first entry
+  ## being the `name` of a fields in the JsObject and the second being its
+  ## value wrapped into a JsObject.
+  var k: cstring
+  var v: JsObject
+  {.emit: "for (var `k` in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) continue;".}
+  {.emit: "  `v`=`obj`[`k`];".}
+  yield (k, v)
+  {.emit: "}".}
+
+iterator items*(obj: JsObject): JsObject =
+  ## Yields the `values` of each field in a JsObject, wrapped into a JsObject.
+  var v: JsObject
+  {.emit: "for (var k in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(k)) continue;".}
+  {.emit: "  `v`=`obj`[k];".}
+  yield v
+  {.emit: "}".}
+
+iterator keys*(obj: JsObject): cstring =
+  ## Yields the `names` of each field in a JsObject.
+  var k: cstring
+  {.emit: "for (var `k` in `obj`) {".}
+  {.emit: "  if (!`obj`.hasOwnProperty(`k`)) continue;".}
+  yield k
+  {.emit: "}".}
+
+iterator pairs*[K, V](assoc: JsAssoc[K, V]): (K,V) =
+  ## Yields tuples of type ``(K, V)``, with the first entry
+  ## being a `key` in the JsAssoc and the second being its corresponding value.
+  when K is string:
+    var k: cstring
+  else:
+    var k: K
+  var v: V
+  {.emit: "for (var `k` in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) continue;".}
+  {.emit: "  `v`=`assoc`[`k`];".}
+  when K is string:
+    yield ($k, v)
+  else:
+    yield (k, v)
+  {.emit: "}".}
+
+iterator items*[K,V](assoc: JSAssoc[K,V]): V =
+  ## Yields the `values` in a JsAssoc.
+  var v: V
+  {.emit: "for (var k in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(k)) continue;".}
+  {.emit: "  `v`=`assoc`[k];".}
+  yield v
+  {.emit: "}".}
+
+iterator keys*[K,V](assoc: JSAssoc[K,V]): K =
+  ## Yields the `keys` in a JsAssoc.
+  when K is string:
+    var k: cstring
+  else:
+    var k: K
+  {.emit: "for (var `k` in `assoc`) {".}
+  {.emit: "  if (!`assoc`.hasOwnProperty(`k`)) continue;".}
+  when K is string:
+    yield $k
+  else:
+    yield k
+  {.emit: "}".}
+
+# Literal generation
+
+macro `{}`*(typ: typedesc, xs: varargs[untyped]): auto =
+  ## Takes a ``typedesc`` as its first argument, and a series of expressions of
+  ## type ``key: value``, and returns a value of the specified type with each
+  ## field ``key`` set to ``value``, as specified in the arguments of ``{}``.
+  ##
+  ## Example:
+  ##
+  ## .. code-block:: nim
+  ##
+  ##  # Let's say we have a type with a ton of fields, where some fields do not
+  ##  # need to be set, and we do not want those fields to be set to ``nil``:
+  ##  type
+  ##    ExtremelyHugeType = ref object
+  ##      a, b, c, d, e, f, g: int
+  ##      h, i, j, k, l: cstring
+  ##      # And even more fields ...
+  ##
+  ##  let obj = ExtremelyHugeType{ a: 1, k: "foo".cstring, d: 42 }
+  ##
+  ##  # This generates roughly the same JavaScript as:
+  ##  {. emit: "var obj = {a: 1, k: "foo", d: 42};" .}
+  ##
+  let a = !"a"
+  var body = quote do:
+    var `a` {.noinit.}: `typ`
+    {.emit: "`a` = {};".}
+  for x in xs.children:
+    if x.kind == nnkExprColonExpr:
+      let
+        k = x[0]
+        kString = quote do:
+          when compiles($`k`): $`k` else: "invalid"
+        v = x[1]
+      body.add(quote do:
+        when compiles(`a`.`k`):
+          `a`.`k` = `v`
+        elif compiles(`a`[`k`]):
+          `a`[`k`] = `v`
+        else:
+          `a`[`kString`] = `v`
+      )
+    else:
+      error("Expression `" & $x.toStrLit & "` not allowed in `{}` macro")
+
+  body.add(quote do:
+    return `a`
+  )
+
+  result = quote do:
+    proc inner(): `typ` {.gensym.} =
+      `body`
+    inner()
+
+# Macro to build a lambda using JavaScript's `this`
+# from a proc, `this` being the first argument.
+
+macro bindMethod*(procedure: typed): auto =
+  ## Takes the name of a procedure and wraps it into a lambda missing the first
+  ## argument, which passes the JavaScript builtin ``this`` as the first
+  ## argument to the procedure. Returns the resulting lambda.
+  ##
+  ## Example:
+  ##
+  ## We want to generate roughly this JavaScript:
+  ##
+  ## .. code-block:: js
+  ##  var obj = {a: 10};
+  ##  obj.someMethod = function() {
+  ##    return this.a + 42;
+  ##  };
+  ##
+  ## We can achieve this using the ``bindMethod`` macro:
+  ##
+  ## .. code-block:: nim
+  ##  let obj = JsObject{ a: 10 }
+  ##  proc someMethodImpl(that: JsObject): int =
+  ##    that.a.to(int) + 42
+  ##  obj.someMethod = bindMethod someMethodImpl
+  ##
+  ##  # Alternatively:
+  ##  obj.someMethod = bindMethod
+  ##    proc(that: JsObject): int = that.a.to(int) + 42
+  if not (procedure.kind == nnkSym or procedure.kind == nnkLambda):
+    error("Argument has to be a proc or a symbol corresponding to a proc.")
+  var
+    rawProc = if procedure.kind == nnkSym:
+        getImpl(procedure.symbol)
+      else:
+        procedure
+    args = rawProc[3]
+    thisType = args[1][1]
+    params = newNimNode(nnkFormalParams).add(args[0])
+    body = newNimNode(nnkLambda)
+    this = newIdentNode("this")
+    # construct the `this` parameter:
+    thisQuote = quote do:
+      var `this` {. nodecl, importc .} : `thisType`
+    call = newNimNode(nnkCall).add(rawProc[0], thisQuote[0][0][0][0])
+  # construct the procedure call inside the method
+  if args.len > 2:
+    for idx in 2..args.len-1:
+      params.add(args[idx])
+      call.add(args[idx][0])
+  body.add(newNimNode(nnkEmpty),
+      rawProc[1],
+      rawProc[2],
+      params,
+      rawProc[4],
+      rawProc[5],
+      newTree(nnkStmtList, thisQuote[0], call)
+  )
+  result = body
diff --git a/tests/js/tjsffi.nim b/tests/js/tjsffi.nim
new file mode 100644
index 000000000..71eb211e3
--- /dev/null
+++ b/tests/js/tjsffi.nim
@@ -0,0 +1,267 @@
+discard """
+  output: '''true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true
+true'''
+"""
+
+import macros, jsffi
+
+# Tests for JsObject
+# Test JsObject []= and []
+block:
+  proc test(): bool =
+    let obj = newJsObject()
+    var working = true
+    obj["a"] = 11
+    obj["b"] = "test"
+    obj["c"] = "test".cstring
+    working = working and obj["a"].to(int) == 11
+    working = working and obj["c"].to(cstring) == "test".cstring
+    working
+  echo test()
+
+# Test JsObject .= and .
+block:
+  proc test(): bool =
+    let obj = newJsObject()
+    var working = true
+    obj.a = 11
+    obj.b = "test"
+    obj.c = "test".cstring
+    obj.`$!&` = 42
+    obj.`while` = 99
+    working = working and obj.a.to(int) == 11
+    working = working and obj.b.to(string) == "test"
+    working = working and obj.c.to(cstring) == "test".cstring
+    working = working and obj.`$!&`.to(int) == 42
+    working = working and obj.`while`.to(int) == 99
+    working
+  echo test()
+
+# Test JsObject .()
+block:
+  proc test(): bool =
+    let obj = newJsObject()
+    obj.`?!$` = proc(x, y, z: int, t: string): string = t & $(x + y + z)
+    obj.`?!$`(1, 2, 3, "Result is: ").to(string) == "Result is: 6"
+  echo test()
+
+# Test JsObject []()
+block:
+  proc test(): bool =
+    let obj = newJsObject()
+    obj.a = proc(x, y, z: int, t: string): string = t & $(x + y + z)
+    let call = obj["a"].to(proc(x, y, z: int, t: string): string)
+    call(1, 2, 3, "Result is: ") == "Result is: 6"
+  echo test()
+
+# Test JsObject Iterators
+block:
+  proc testPairs(): bool =
+    let obj = newJsObject()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for k, v in obj.pairs:
+      case $k
+      of "a":
+        working = working and v.to(int) == 10
+      of "b":
+        working = working and v.to(int) == 20
+      of "c":
+        working = working and v.to(int) == 30
+      else:
+        return false
+    working
+  proc testItems(): bool =
+    let obj = newJsObject()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for v in obj.items:
+      working = working and v.to(int) in [10, 20, 30]
+    working
+  proc testKeys(): bool =
+    let obj = newJsObject()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for v in obj.keys:
+      working = working and $v in ["a", "b", "c"]
+    working
+  proc test(): bool = testPairs() and testItems() and testKeys()
+  echo test()
+
+# Test JsObject equality
+block:
+  proc test(): bool =
+    {. emit: "var comparison = {a: 22, b: 'test'};" .}
+    var comparison {. importc, nodecl .}: JsObject
+    let obj = newJsObject()
+    obj.a = 22
+    obj.b = "test".cstring
+    obj.a == comparison.a and obj.b == comparison.b
+  echo test()
+
+# Test JsObject literal
+block:
+  proc test(): bool =
+    {. emit: "var comparison = {a: 22, b: 'test'};" .}
+    var comparison {. importc, nodecl .}: JsObject
+    let obj = JsObject{ a: 22, b: "test".cstring }
+    obj.a == comparison.a and obj.b == comparison.b
+  echo test()
+
+# Tests for JsAssoc
+# Test JsAssoc []= and []
+block:
+  proc test(): bool =
+    let obj = newJsAssoc[int, int]()
+    var working = true
+    obj[1] = 11
+    working = working and not compiles(obj["a"] = 11)
+    working = working and not compiles(obj["a"])
+    working = working and not compiles(obj[2] = "test")
+    working = working and not compiles(obj[3] = "test".cstring)
+    working = working and obj[1] == 11
+    working
+  echo test()
+
+# Test JsAssoc .= and .
+block:
+  proc test(): bool =
+    let obj = newJsAssoc[string, int]()
+    var working = true
+    obj.a = 11
+    obj.`$!&` = 42
+    working = working and not compiles(obj.b = "test")
+    working = working and not compiles(obj.c = "test".cstring)
+    working = working and obj.a == 11
+    working = working and obj.`$!&` == 42
+    working
+  echo test()
+
+# Test JsAssoc .()
+block:
+  proc test(): bool =
+    let obj = newJsAssoc[string, proc(e: int): int]()
+    obj.a = proc(e: int): int = e * e
+    obj.a(10) == 100
+  echo test()
+
+# Test JsAssoc []()
+block:
+  proc test(): bool =
+    let obj = newJsAssoc[string, proc(e: int): int]()
+    obj.a = proc(e: int): int = e * e
+    let call = obj["a"]
+    call(10) == 100
+  echo test()
+
+# Test JsAssoc Iterators
+block:
+  proc testPairs(): bool =
+    let obj = newJsAssoc[string, int]()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for k, v in obj.pairs:
+      case $k
+      of "a":
+        working = working and v == 10
+      of "b":
+        working = working and v == 20
+      of "c":
+        working = working and v == 30
+      else:
+        return false
+    working
+  proc testItems(): bool =
+    let obj = newJsAssoc[string, int]()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for v in obj.items:
+      working = working and v in [10, 20, 30]
+    working
+  proc testKeys(): bool =
+    let obj = newJsAssoc[string, int]()
+    var working = true
+    obj.a = 10
+    obj.b = 20
+    obj.c = 30
+    for v in obj.keys:
+      working = working and v in ["a", "b", "c"]
+    working
+  proc test(): bool = testPairs() and testItems() and testKeys()
+  echo test()
+
+# Test JsAssoc equality
+block:
+  proc test(): bool =
+    {. emit: "var comparison = {a: 22, b: 55};" .}
+    var comparison {. importcpp, nodecl .}: JsAssoc[string, int]
+    let obj = newJsAssoc[string, int]()
+    obj.a = 22
+    obj.b = 55
+    obj.a == comparison.a and obj.b == comparison.b
+  echo test()
+
+# Test JsAssoc literal
+block:
+  proc test(): bool =
+    {. emit: "var comparison = {a: 22, b: 55};" .}
+    var comparison {. importcpp, nodecl .}: JsAssoc[string, int]
+    let obj = JsAssoc[string, int]{ a: 22, b: 55 }
+    var working = true
+    working = working and
+      compiles(JsAssoc[int, int]{ 1: 22, 2: 55 })
+    working = working and
+      comparison.a == obj.a and comparison.b == obj.b
+    working = working and
+      not compiles(JsAssoc[string, int]{ a: "test" })
+    working
+  echo test()
+
+# Tests for macros on non-JsRoot objects
+# Test lit
+block:
+  type TestObject = object
+    a: int
+    b: cstring
+  proc test(): bool =
+    {. emit: "var comparison = {a: 1};" .}
+    var comparison {. importc, nodecl .}: TestObject
+    let obj = TestObject{ a: 1 }
+    obj == comparison
+  echo test()
+
+# Test bindMethod
+block:
+  type TestObject = object
+    a: int
+    onWhatever: proc(e: int): int
+  proc handleWhatever(that: TestObject, e: int): int =
+    e + that.a
+  proc test(): bool =
+    let obj = TestObject(a: 9, onWhatever: bindMethod(handleWhatever))
+    obj.onWhatever(1) == 10
+  echo test()
diff --git a/web/website.ini b/web/website.ini
index 0405acd08..a3a04f01e 100644
--- a/web/website.ini
+++ b/web/website.ini
@@ -39,7 +39,7 @@ srcdoc2: "impure/re;impure/nre;pure/typetraits"
 srcdoc2: "pure/concurrency/threadpool.nim;pure/concurrency/cpuinfo.nim"
 srcdoc: "system/threads.nim;system/channels.nim;js/dom"
 srcdoc2: "pure/os;pure/strutils;pure/math;pure/matchers;pure/algorithm"
-srcdoc2: "pure/stats;impure/nre;windows/winlean;pure/random"
+srcdoc2: "pure/stats;impure/nre;windows/winlean;pure/random;js/jsffi"
 srcdoc2: "pure/complex;pure/times;pure/osproc;pure/pegs;pure/dynlib;pure/strscans"
 srcdoc2: "pure/parseopt;pure/parseopt2;pure/hashes;pure/strtabs;pure/lexbase"
 srcdoc2: "pure/parsecfg;pure/parsexml;pure/parsecsv;pure/parsesql"