From 02ca87bc05b2f4027a08d0197e76f1e3f55b8ab0 Mon Sep 17 00:00:00 2001 From: Petr Pucil Date: Wed, 14 Feb 2024 21:01:07 +0100 Subject: [PATCH] JS: enable circular imports by exporting object (not function) Resolves https://github.com/kaitai-io/kaitai_struct/issues/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 https://github.com/kaitai-io/kaitai_struct/issues/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. --- .../struct/languages/JavaScriptCompiler.scala | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala index 17d6db711..0a72f6ee8 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala @@ -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 = { @@ -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 = { @@ -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) @@ -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)" } }