Skip to content

Commit

Permalink
JS: enable circular imports by exporting object (not function)
Browse files Browse the repository at this point in the history
Resolves kaitai-io/kaitai_struct#1074

This change breaks backward compatibility with 0.10 and older, but
allows for circular imports (in all JavaScript module systems) and
out-of-order module loading in a "browser globals" context (the latter
is particularly relevant in the Web IDE, as explained at
kaitai-io/kaitai_struct#1074). In short, it
does this by switching from the UMD envelope
[returnExports.js](https://github.com/umdjs/umd/blob/36fd113/templates/returnExports.js#L17-L37)
to modified
[commonjsStrict.js](https://github.com/umdjs/umd/blob/36fd113/templates/commonjsStrict.js#L19-L36).

The BC break is that until now the generated modules exported the
constructor function directly, whereas now they export the object
containing the constructor function under the only object key that
matches the format module name. The same behavior is expected from
imported opaque types and custom processors as well.
  • Loading branch information
generalmimon committed Feb 21, 2024
1 parent b6756f7 commit 1c1a348
Showing 1 changed file with 23 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
override def outImports(topClass: ClassSpec) = {
val impList = importList.toList
val quotedImpList = impList.map((x) => s"'$x'")
val defineArgs = quotedImpList.mkString(", ")
val moduleArgs = quotedImpList.map((x) => s"require($x)").mkString(", ")
val argClasses = impList.map((x) => x.split('/').last)
val rootArgs = argClasses.map((x) => s"root.$x").mkString(", ")
val defineArgs = ("'exports'" +: quotedImpList).mkString(", ")
val exportsArgs = ("exports" +: quotedImpList.map((x) => s"require($x)")).mkString(", ")
val argClasses = types2class(topClass.name) +: impList.map((x) => x.split('/').last)
val rootArgs = argClasses.map((x) => if (x == "KaitaiStream") s"root.$x" else s"root.$x || (root.$x = {})").mkString(", ")
val factoryParams = argClasses.map((x) => if (x == "KaitaiStream") x else s"${x}_").mkString(", ")

"(function (root, factory) {\n" +
indent + "if (typeof define === 'function' && define.amd) {\n" +
indent * 2 + s"define([$defineArgs], factory);\n" +
indent + "} else if (typeof module === 'object' && module.exports) {\n" +
indent * 2 + s"module.exports = factory($moduleArgs);\n" +
indent + "} else if (typeof exports === 'object' && exports !== null && typeof exports.nodeType !== 'number') {\n" +
indent * 2 + s"factory($exportsArgs);\n" +
indent + "} else {\n" +
indent * 2 + s"root.${types2class(topClass.name)} = factory($rootArgs);\n" +
indent * 2 + s"factory($rootArgs);\n" +
indent + "}\n" +
s"}(typeof self !== 'undefined' ? self : this, function (${argClasses.mkString(", ")}) {"
s"})(typeof self !== 'undefined' ? self : this, function ($factoryParams) {"
}

override def fileHeader(topClassName: String): Unit = {
Expand All @@ -53,8 +54,8 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
}

override def fileFooter(name: String): Unit = {
out.puts(s"return ${type2class(name)};")
out.puts("}));")
out.puts(s"${type2class(name)}_.${type2class(name)} = ${type2class(name)};")
out.puts("});")
}

override def opaqueClassDeclaration(classSpec: ClassSpec): Unit = {
Expand Down Expand Up @@ -215,7 +216,7 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)

importList.add(s"$pkgName$procClass")

out.puts(s"var _process = new $procClass(${args.map(expression).mkString(", ")});")
out.puts(s"var _process = new ${procClass}_.${procClass}(${args.map(expression).mkString(", ")});")
s"_process.decode($srcExpr)"
}
handleAssignment(varDest, expr, rep, false)
Expand Down Expand Up @@ -379,7 +380,17 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
case _ => ""
}
val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "")
s"new ${types2class(t.name)}($io, $parent, $root$addEndian$addParams)"
// If the first segment of the name path refers to a top-level type, we
// must prepend the name of top-level module (which ends with an
// underscore `_` according to our own convention for clarity) before the
// path because of https://github.com/kaitai-io/kaitai_struct/issues/1074
val topLevelModulePrefix =
if (t.classSpec.map((classSpec) => t.name == classSpec.name).getOrElse(false)) {
s"${type2class(t.name(0))}_."
} else {
""
}
s"new ${topLevelModulePrefix}${types2class(t.name)}($io, $parent, $root$addEndian$addParams)"
}
}

Expand Down

0 comments on commit 1c1a348

Please sign in to comment.