From c7634ad4290a301e4736e55cc7108754f0150606 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Wed, 23 Jul 2025 12:10:37 -0500 Subject: [PATCH 01/11] Update build.sbt to read NetLogo and parserJS version from ENV with fallback --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 94b8d3f3f..2bb0256e9 100644 --- a/build.sbt +++ b/build.sbt @@ -2,9 +2,9 @@ import sbtcrossproject.CrossPlugin.autoImport.CrossType import sbtcrossproject.CrossProject import org.scalajs.sbtplugin.ScalaJSCrossVersion -val nlDependencyVersion = "7.0.0-beta2-8cd3e65" +val nlDependencyVersion = sys.env.getOrElse("NL_DEPENDENCY_VERSION", "7.0.0-beta2-8cd3e65") -val parserJsDependencyVersion = "0.4.0-8cd3e65" +val parserJsDependencyVersion = sys.env.getOrElse("PARSER_JS_DEPENDENCY_VERSION", "0.4.0-8cd3e65") val scalazVersion = "7.2.36" From 18475baef7f1e8964f19782c4e02055ebacd7c22 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Wed, 23 Jul 2025 13:53:30 -0500 Subject: [PATCH 02/11] Extensions: Modify NLWExtensionManager to pass URLs to importExtension --- compiler/shared/src/main/scala/NLWExtensionManager.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/shared/src/main/scala/NLWExtensionManager.scala b/compiler/shared/src/main/scala/NLWExtensionManager.scala index fc7a9b628..e94647b36 100644 --- a/compiler/shared/src/main/scala/NLWExtensionManager.scala +++ b/compiler/shared/src/main/scala/NLWExtensionManager.scala @@ -195,6 +195,9 @@ class NLWExtensionManager extends ExtensionManager { override def finishFullCompilation(): Unit = () override def importExtension(extName: String, errors: ErrorSource): Unit = { + importExtension(extName, None, errors) + } + override def importExtension(extName: String, extUrl: Option[String], errors: ErrorSource): Unit = { val extension = extNameToExtMap.getOrElse(extName, failCompilation(s"No such extension: ${extName}")) val extPrimPairs = extension.getPrims.map(prim => (extension.getName, prim)) val shoutedPairs = extPrimPairs.map { case (extName, ExtensionPrim(prim, name)) => (s"$extName:$name".toUpperCase, prim) } From 7a72dc06d4b5ea4ac75c727ff617a6a030d57499 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Wed, 23 Jul 2025 13:53:43 -0500 Subject: [PATCH 03/11] Extensions: Export listExtensions js function --- .../js/src/main/scala/BrowserCompiler.scala | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/compiler/js/src/main/scala/BrowserCompiler.scala b/compiler/js/src/main/scala/BrowserCompiler.scala index 0fb50498c..9a70ffe53 100644 --- a/compiler/js/src/main/scala/BrowserCompiler.scala +++ b/compiler/js/src/main/scala/BrowserCompiler.scala @@ -14,6 +14,8 @@ import json.TortoiseJson._ import json.WidgetToJson import org.nlogo.core.{ CompilerException, Plot } +import org.nlogo.parse.FrontEnd + import org.nlogo.tortoise.compiler.xml.TortoiseModelLoader import org.nlogo.parse.CompilerUtilities @@ -30,6 +32,7 @@ import scalaz.std.list._ import scalaz.Scalaz.ToValidationOps import scalaz.Validation.FlatMap.ValidationFlatMapRequested + // scalastyle:off number.of.methods @JSExportTopLevel("BrowserCompiler") class BrowserCompiler { @@ -220,6 +223,23 @@ class BrowserCompiler { } + @JSExport + def listExtensions(source: String): NativeJson = { + val extensions: Seq[(String, Option[String])] = FrontEnd.findAllExtensions(source); + val json = JsArray( + extensions.map { + case (name, url) => + JsObject( + ListMap("name" -> JsString(name), "url" -> (url match { + case Some(u) => JsString(u) + case None => JsNull + })) + ) + } + ) + JsonLibrary.toNative(json) + } + @JSExport def listProcedures(): NativeJson = { From dbe7c43d51798e350fb2802357868e9390918f77 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 13:59:11 -0500 Subject: [PATCH 04/11] Extnesions: Expose NLWExtensionsLoader to Scala --- .../src/main/scala/NLWExtensionsLoader.scala | 29 +++++++++++++++++++ .../src/main/scala/NLWExtensionsLoader.scala | 25 ++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 compiler/js/src/main/scala/NLWExtensionsLoader.scala create mode 100644 compiler/jvm/src/main/scala/NLWExtensionsLoader.scala diff --git a/compiler/js/src/main/scala/NLWExtensionsLoader.scala b/compiler/js/src/main/scala/NLWExtensionsLoader.scala new file mode 100644 index 000000000..89f7f2cff --- /dev/null +++ b/compiler/js/src/main/scala/NLWExtensionsLoader.scala @@ -0,0 +1,29 @@ +// This file is related to the NetLogo Web (NLW) project +// Please see org:NetLogo/Galapagos/app/assets/javascript/beak/nlw-extensions-loader.js +// for more details. This change was introduced in https://github.com/NetLogo/NetLogo/pull/2508 +// Omar Ibrahim, 2025 +package org.nlogo.tortoise.compiler + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSGlobal +import play.api.libs.json.{ JsValue, Json, JsNull } + +// window.NLWExtensionsLoader is a global object and is +// gauranteed to be available before +@js.native +@JSGlobal("NLWExtensionsLoader") +object NLWExtensionsLoader extends js.Object { + def getPrimitivesFromURL(url: String): js.Object = js.native +} + +object WrappedNLWExtensionsLoader { + def getPrimitivesFromURL(url: String): Option[JsValue] = { + val result = NLWExtensionsLoader.getPrimitivesFromURL(url) + if (result == null || result == js.undefined) { + None + } else { + val json = Json.parse(js.JSON.stringify(result)) + Some(json) + } + } +} \ No newline at end of file diff --git a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala new file mode 100644 index 000000000..2611b6e41 --- /dev/null +++ b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala @@ -0,0 +1,25 @@ +// This file is related to the NetLogo Web (NLW) project +// Please see org:NetLogo/Galapagos/app/assets/javascript/beak/nlw-extensions-loader.js +// for more details. This change was introduced in https://github.com/NetLogo/NetLogo/pull/2508 +// This is not supported in the JVM compiler. +// Omar Ibrahim, 2025 +package org.nlogo.tortoise.compiler +import scala.annotation.unused + +import play.api.libs.json.JsValue + +object NLWExtensionsLoader { + def getPrimitivesFromURL(@unused url: String): JsValue = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + // In a real implementation, this would access a preloaded cache of extensions. + // For example, in a JavaScript environment, it might look like: + // val cache = js.Dynamic.global.extensionCache + } +} + + +object WrappedNLWExtensionsLoader { + def getPrimitivesFromURL(url: String): Option[JsValue] = { + throw new UnsupportedOperationException("WrappedNLWExtensionsLoader is not implemented in the JVM environment.") + } +} From 525a5473662c96233914671acf6bb6318905dd42 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 13:59:32 -0500 Subject: [PATCH 05/11] Extensions: Integrate NLWExtensionsLoader in NLWExtensionsManager --- .../src/main/scala/NLWExtensionManager.scala | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/compiler/shared/src/main/scala/NLWExtensionManager.scala b/compiler/shared/src/main/scala/NLWExtensionManager.scala index e94647b36..4a4e82d59 100644 --- a/compiler/shared/src/main/scala/NLWExtensionManager.scala +++ b/compiler/shared/src/main/scala/NLWExtensionManager.scala @@ -59,6 +59,10 @@ object CreateExtension { def apply(json: String): Extension = { val jsExt = Json.parse(json) + fromJSON(jsExt) + } + + def fromJSON(jsExt: JsValue): Extension = { new Extension { override def getName: String = { jsExt("name").as[String] @@ -197,12 +201,23 @@ class NLWExtensionManager extends ExtensionManager { override def importExtension(extName: String, errors: ErrorSource): Unit = { importExtension(extName, None, errors) } - override def importExtension(extName: String, extUrl: Option[String], errors: ErrorSource): Unit = { - val extension = extNameToExtMap.getOrElse(extName, failCompilation(s"No such extension: ${extName}")) + override def importExtension(extName: String, extURL: Option[String], errors: ErrorSource): Unit = { + val extension = extURL match { + case Some(url) => + val extObj = WrappedNLWExtensionsLoader.getPrimitivesFromURL(url) + extObj match { + case Some(obj) => CreateExtension.fromJSON(obj) + case None => failCompilation(s"Could not load extension from URL: ${url}") + } + case None => extNameToExtMap.getOrElse(extName, failCompilation(s"No such extension: ${extName}")) + } val extPrimPairs = extension.getPrims.map(prim => (extension.getName, prim)) val shoutedPairs = extPrimPairs.map { case (extName, ExtensionPrim(prim, name)) => (s"$extName:$name".toUpperCase, prim) } primNameToPrimMap ++= shoutedPairs - importedExtensions.add(extName) + importedExtensions.add(extURL match { + case Some(url) => "url://" + url + case None => extName + }) () } From c6fc9bce44c13a46a6e63bb48879cd6c93bca47d Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 13:59:47 -0500 Subject: [PATCH 06/11] Extensions: Allow URL extensions import from URLExtensionsRepo in Galapagos --- engine/src/main/coffee/extensions/all.coffee | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/engine/src/main/coffee/extensions/all.coffee b/engine/src/main/coffee/extensions/all.coffee index ce25e72e8..595393e09 100644 --- a/engine/src/main/coffee/extensions/all.coffee +++ b/engine/src/main/coffee/extensions/all.coffee @@ -5,6 +5,7 @@ extensionPaths = ['array', 'bitmap', 'codap', 'csv', 'encode', 'dialog', 'export module.exports = { initialize: (workspace, importedExtensions...) -> + console.log "Initializing extensions: #{importedExtensions.join(', ')}" upperNames = importedExtensions.map( (name) -> name.toUpperCase() ) extensions = {} extensionPaths.forEach( (path) -> @@ -14,6 +15,15 @@ module.exports = { if upperNames.includes(upperName) extensions[upperName] = extension ) + importedExtensions.filter(NLWExtensionsLoader.isURL).forEach( (url) -> + extensionModule = NLWExtensionsLoader.getExtensionModuleFromURL(url) + if extensionModule? + extension = extensionModule.init(workspace) + upperName = extension.name.toUpperCase() + extensions[upperName] = extension + else + console.warn "Extension at URL #{url} does not have an init function." + ) extensions porters: (importedExtensions...) -> @@ -26,6 +36,14 @@ module.exports = { if upperNames.includes(upperName) porters.push(extensionModule.porter) ) + importedExtensions.filter(NLWExtensionsLoader.isURL).forEach( (url) -> + extensionModule = NLWExtensionsLoader.getExtensionModuleFromURL(url) + if extensionModule? and extensionModule.porter? + upperName = extensionModule.porter.extensionName.toUpperCase() + porters.push(extensionModule.porter) + else + console.warn "Extension at URL #{url} does not have an init function." + ) porters } From 93821cb4d68699c69a05d306f8941e417ba81b94 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 16:18:59 -0500 Subject: [PATCH 07/11] Extensions: Remove unnecessary definition for the JVM --- compiler/jvm/src/main/scala/NLWExtensionsLoader.scala | 9 --------- 1 file changed, 9 deletions(-) diff --git a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala index 2611b6e41..8f986c7d6 100644 --- a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala @@ -8,15 +8,6 @@ import scala.annotation.unused import play.api.libs.json.JsValue -object NLWExtensionsLoader { - def getPrimitivesFromURL(@unused url: String): JsValue = { - throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") - // In a real implementation, this would access a preloaded cache of extensions. - // For example, in a JavaScript environment, it might look like: - // val cache = js.Dynamic.global.extensionCache - } -} - object WrappedNLWExtensionsLoader { def getPrimitivesFromURL(url: String): Option[JsValue] = { From cd3e9883bd937f4303a46c624d8460a72c8d25b5 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 16:19:18 -0500 Subject: [PATCH 08/11] Extensions: Fix incorrect scala.js type in NLWExtensionsLoader --- compiler/js/src/main/scala/NLWExtensionsLoader.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/js/src/main/scala/NLWExtensionsLoader.scala b/compiler/js/src/main/scala/NLWExtensionsLoader.scala index 89f7f2cff..cc0e65bb2 100644 --- a/compiler/js/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/js/src/main/scala/NLWExtensionsLoader.scala @@ -6,14 +6,14 @@ package org.nlogo.tortoise.compiler import scala.scalajs.js import scala.scalajs.js.annotation.JSGlobal -import play.api.libs.json.{ JsValue, Json, JsNull } +import play.api.libs.json.{ JsValue, Json } // window.NLWExtensionsLoader is a global object and is // gauranteed to be available before @js.native @JSGlobal("NLWExtensionsLoader") object NLWExtensionsLoader extends js.Object { - def getPrimitivesFromURL(url: String): js.Object = js.native + def getPrimitivesFromURL(url: String): js.UndefOr[js.Object] = js.native } object WrappedNLWExtensionsLoader { From f995331ab5823330ca100ef7c865c5ef9ad266e1 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 16:21:38 -0500 Subject: [PATCH 09/11] Extensions: Rename WrappedNLWExtensionsManager to simply NLWExtensionsManager --- compiler/js/src/main/scala/NLWExtensionsLoader.scala | 11 ++++++++--- compiler/jvm/src/main/scala/NLWExtensionsLoader.scala | 9 ++++++--- .../shared/src/main/scala/NLWExtensionManager.scala | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/compiler/js/src/main/scala/NLWExtensionsLoader.scala b/compiler/js/src/main/scala/NLWExtensionsLoader.scala index cc0e65bb2..d274fdc81 100644 --- a/compiler/js/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/js/src/main/scala/NLWExtensionsLoader.scala @@ -12,13 +12,14 @@ import play.api.libs.json.{ JsValue, Json } // gauranteed to be available before @js.native @JSGlobal("NLWExtensionsLoader") -object NLWExtensionsLoader extends js.Object { +object JSNLWExtensionsLoader extends js.Object { def getPrimitivesFromURL(url: String): js.UndefOr[js.Object] = js.native + def appendURLProtocol(url: String): String = js.native } -object WrappedNLWExtensionsLoader { +object NLWExtensionsLoader { def getPrimitivesFromURL(url: String): Option[JsValue] = { - val result = NLWExtensionsLoader.getPrimitivesFromURL(url) + val result = JSNLWExtensionsLoader.getPrimitivesFromURL(url) if (result == null || result == js.undefined) { None } else { @@ -26,4 +27,8 @@ object WrappedNLWExtensionsLoader { Some(json) } } + + def appendURLProtocol(url: String): String = { + JSNLWExtensionsLoader.appendURLProtocol(url) + } } \ No newline at end of file diff --git a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala index 8f986c7d6..4929231a6 100644 --- a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala @@ -9,8 +9,11 @@ import scala.annotation.unused import play.api.libs.json.JsValue -object WrappedNLWExtensionsLoader { - def getPrimitivesFromURL(url: String): Option[JsValue] = { - throw new UnsupportedOperationException("WrappedNLWExtensionsLoader is not implemented in the JVM environment.") +object NLWExtensionsLoader { + def getPrimitivesFromURL(@unused url: String): Option[JsValue] = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + } + def appendURLProtocol(@unused url: String): String = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") } } diff --git a/compiler/shared/src/main/scala/NLWExtensionManager.scala b/compiler/shared/src/main/scala/NLWExtensionManager.scala index 4a4e82d59..150038424 100644 --- a/compiler/shared/src/main/scala/NLWExtensionManager.scala +++ b/compiler/shared/src/main/scala/NLWExtensionManager.scala @@ -204,7 +204,7 @@ class NLWExtensionManager extends ExtensionManager { override def importExtension(extName: String, extURL: Option[String], errors: ErrorSource): Unit = { val extension = extURL match { case Some(url) => - val extObj = WrappedNLWExtensionsLoader.getPrimitivesFromURL(url) + val extObj = NLWExtensionsLoader.getPrimitivesFromURL(url) extObj match { case Some(obj) => CreateExtension.fromJSON(obj) case None => failCompilation(s"Could not load extension from URL: ${url}") @@ -215,7 +215,7 @@ class NLWExtensionManager extends ExtensionManager { val shoutedPairs = extPrimPairs.map { case (extName, ExtensionPrim(prim, name)) => (s"$extName:$name".toUpperCase, prim) } primNameToPrimMap ++= shoutedPairs importedExtensions.add(extURL match { - case Some(url) => "url://" + url + case Some(url) => NLWExtensionsLoader.appendURLProtocol(url) case None => extName }) () From 7a3d1779bc343447c43102539efb850f686f680d Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 24 Jul 2025 16:31:58 -0500 Subject: [PATCH 10/11] Extensions: Add URL validation --- compiler/js/src/main/scala/NLWExtensionsLoader.scala | 5 +++++ compiler/jvm/src/main/scala/NLWExtensionsLoader.scala | 3 +++ compiler/shared/src/main/scala/NLWExtensionManager.scala | 3 +++ 3 files changed, 11 insertions(+) diff --git a/compiler/js/src/main/scala/NLWExtensionsLoader.scala b/compiler/js/src/main/scala/NLWExtensionsLoader.scala index d274fdc81..e4748f220 100644 --- a/compiler/js/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/js/src/main/scala/NLWExtensionsLoader.scala @@ -15,6 +15,7 @@ import play.api.libs.json.{ JsValue, Json } object JSNLWExtensionsLoader extends js.Object { def getPrimitivesFromURL(url: String): js.UndefOr[js.Object] = js.native def appendURLProtocol(url: String): String = js.native + def validateURL(url: String): Boolean = js.native } object NLWExtensionsLoader { @@ -31,4 +32,8 @@ object NLWExtensionsLoader { def appendURLProtocol(url: String): String = { JSNLWExtensionsLoader.appendURLProtocol(url) } + + def validateURL(url: String): Boolean = { + JSNLWExtensionsLoader.validateURL(url) + } } \ No newline at end of file diff --git a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala index 4929231a6..fff2f6c25 100644 --- a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala +++ b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala @@ -16,4 +16,7 @@ object NLWExtensionsLoader { def appendURLProtocol(@unused url: String): String = { throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") } + def validateURL(@unused url: String): Boolean = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + } } diff --git a/compiler/shared/src/main/scala/NLWExtensionManager.scala b/compiler/shared/src/main/scala/NLWExtensionManager.scala index 150038424..aca54955d 100644 --- a/compiler/shared/src/main/scala/NLWExtensionManager.scala +++ b/compiler/shared/src/main/scala/NLWExtensionManager.scala @@ -204,6 +204,9 @@ class NLWExtensionManager extends ExtensionManager { override def importExtension(extName: String, extURL: Option[String], errors: ErrorSource): Unit = { val extension = extURL match { case Some(url) => + if (!NLWExtensionsLoader.validateURL(url)) { + failCompilation(s"Invalid URL for extension: ${url}") + } val extObj = NLWExtensionsLoader.getPrimitivesFromURL(url) extObj match { case Some(obj) => CreateExtension.fromJSON(obj) From c0f07fc98fc66c69ce8d1908df3567636807e2b5 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Mon, 28 Jul 2025 09:32:52 -0500 Subject: [PATCH 11/11] Extensions: Remove stray console.log statement --- engine/src/main/coffee/extensions/all.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/src/main/coffee/extensions/all.coffee b/engine/src/main/coffee/extensions/all.coffee index 595393e09..5eebf9e61 100644 --- a/engine/src/main/coffee/extensions/all.coffee +++ b/engine/src/main/coffee/extensions/all.coffee @@ -5,7 +5,6 @@ extensionPaths = ['array', 'bitmap', 'codap', 'csv', 'encode', 'dialog', 'export module.exports = { initialize: (workspace, importedExtensions...) -> - console.log "Initializing extensions: #{importedExtensions.join(', ')}" upperNames = importedExtensions.map( (name) -> name.toUpperCase() ) extensions = {} extensionPaths.forEach( (path) ->