diff --git a/docker/registry/server-base/gradle.properties b/docker/registry/server-base/gradle.properties index e7a84a03430..f25f0bf1fcf 100644 --- a/docker/registry/server-base/gradle.properties +++ b/docker/registry/server-base/gradle.properties @@ -1,3 +1,3 @@ io.deephaven.project.ProjectType=DOCKER_REGISTRY deephaven.registry.imageName=ghcr.io/deephaven/server-base:edge -deephaven.registry.imageId=ghcr.io/deephaven/server-base@sha256:bf7d30a09484c40b85f9c2edfa4082dfd8df276295ac636204d42addab6080a5 +deephaven.registry.imageId=ghcr.io/deephaven/server-base@sha256:f774bf3349210a1d44988bde3b6b61c858e4e01f11de9b0b2040b46eceede2c2 diff --git a/docker/registry/slim-base/gradle.properties b/docker/registry/slim-base/gradle.properties index ab9d0b96d3b..78278cf2329 100644 --- a/docker/registry/slim-base/gradle.properties +++ b/docker/registry/slim-base/gradle.properties @@ -1,3 +1,3 @@ io.deephaven.project.ProjectType=DOCKER_REGISTRY deephaven.registry.imageName=ghcr.io/deephaven/server-slim-base:edge -deephaven.registry.imageId=ghcr.io/deephaven/server-slim-base@sha256:6159422839b101500be29884788342201a58ce7b0461179164867b43a0cbfa1a +deephaven.registry.imageId=ghcr.io/deephaven/server-slim-base@sha256:1cdcda42f911ca0dbaf2baf20a8cd17c2136bdaffd9149135c5ce9074913ba95 diff --git a/docker/server-jetty/src/main/server-jetty/requirements.txt b/docker/server-jetty/src/main/server-jetty/requirements.txt index e7383eb3bfe..3ea84669e3f 100644 --- a/docker/server-jetty/src/main/server-jetty/requirements.txt +++ b/docker/server-jetty/src/main/server-jetty/requirements.txt @@ -1,7 +1,7 @@ -adbc-driver-manager==1.11.0 -adbc-driver-postgresql==1.11.0 +adbc-driver-manager==1.12.0 +adbc-driver-postgresql==1.12.0 connectorx==0.4.5; platform.machine == 'x86_64' -deephaven-plugin==0.7.0 +deephaven-plugin==0.8.0 docstring_parser==0.18.0 importlib_resources==7.1.0 java-utilities==0.3.0 @@ -10,10 +10,10 @@ jpy==2.1.0 llvmlite==0.48.0 numba==0.66.0 numpy==2.4.6 -packaging==26.2 -pandas==3.0.3 +packaging==26.3 +pandas==3.0.5 parso==0.8.7 -pyarrow==24.0.0 +pyarrow==25.0.1 python-dateutil==2.9.0.post0 six==1.17.0 -typing_extensions==4.15.0 +typing_extensions==4.16.0 diff --git a/docker/server/src/main/server-netty/requirements.txt b/docker/server/src/main/server-netty/requirements.txt index e7383eb3bfe..3ea84669e3f 100644 --- a/docker/server/src/main/server-netty/requirements.txt +++ b/docker/server/src/main/server-netty/requirements.txt @@ -1,7 +1,7 @@ -adbc-driver-manager==1.11.0 -adbc-driver-postgresql==1.11.0 +adbc-driver-manager==1.12.0 +adbc-driver-postgresql==1.12.0 connectorx==0.4.5; platform.machine == 'x86_64' -deephaven-plugin==0.7.0 +deephaven-plugin==0.8.0 docstring_parser==0.18.0 importlib_resources==7.1.0 java-utilities==0.3.0 @@ -10,10 +10,10 @@ jpy==2.1.0 llvmlite==0.48.0 numba==0.66.0 numpy==2.4.6 -packaging==26.2 -pandas==3.0.3 +packaging==26.3 +pandas==3.0.5 parso==0.8.7 -pyarrow==24.0.0 +pyarrow==25.0.1 python-dateutil==2.9.0.post0 six==1.17.0 -typing_extensions==4.15.0 +typing_extensions==4.16.0 diff --git a/docker/web-plugin-packager/src/main/docker/files/pack-plugins.sh b/docker/web-plugin-packager/src/main/docker/files/pack-plugins.sh index dcee259af07..001222839c0 100755 --- a/docker/web-plugin-packager/src/main/docker/files/pack-plugins.sh +++ b/docker/web-plugin-packager/src/main/docker/files/pack-plugins.sh @@ -16,23 +16,16 @@ TMP_PACKAGE_DIR=${TMP_DIR}/package # Make a output folder mkdir "${OUTPUT_DIR}" -# Start the manifest.json file. It will add all the dependencies automatically -echo "{\"plugins\":[" > "${MANIFEST_FILE}" - -# Keep track of the count so we add a comma when necessary -PLUGIN_COUNT=0 +# Keep track of the directories of all the packed plugins so we can build the manifest.json once everything has +# been downloaded and extracted +PACKAGE_DIRS=() echo "Packing plugins $@..." -# Iterate through each plugin defined in the plugin list, download the package and adding info to the manifest +# Iterate through each plugin defined in the plugin list, download the package and move it into place # Can/should include the version number in the plugin line item for PACKAGE in "$@" do - # Add a comma to the manifest.json if this is not the first plugin - if [ $PLUGIN_COUNT -gt 0 ]; then - echo "," >> "${MANIFEST_FILE}" - fi - # Make a temporary directory for downloading/extracting the package into mkdir -p "${TMP_DIR}" cd "${TMP_DIR}" @@ -53,30 +46,38 @@ do echo "In working dir $(pwd)" - # It always unzips to the folder "package". Get the name and version info from the package so we can move it to - # the correct location and add the info to the manifest + # It always unzips to the folder "package". Get the name from the package so we can move it to the correct + # location PACKAGE_NAME=$(npm pkg get name --prefix="${TMP_PACKAGE_DIR}") - PACKAGE_INFO=$(npm pkg get name version main --prefix="${TMP_PACKAGE_DIR}") # Need to remove quotes from the package name PACKAGE_NAME="${PACKAGE_NAME%\"}" PACKAGE_NAME="${PACKAGE_NAME#\"}" echo "Got package name ${PACKAGE_NAME}" - # Add the info to the manifest file - echo "${PACKAGE_INFO}" >> "${MANIFEST_FILE}" - # Move the plugin to the correct directory # Need to make the directory based on the name first - mkdir -p "${OUTPUT_DIR}/${PACKAGE_NAME}" - mv "${TMP_PACKAGE_DIR}/"* "${OUTPUT_DIR}/${PACKAGE_NAME}" + PACKAGE_DIR=${OUTPUT_DIR}/${PACKAGE_NAME} + mkdir -p "${PACKAGE_DIR}" + mv "${TMP_PACKAGE_DIR}/"* "${PACKAGE_DIR}" rm --recursive "${TMP_DIR}" - - # Increment the plugin count - PLUGIN_COUNT=$((PLUGIN_COUNT + 1)) + PACKAGE_DIRS+=("${PACKAGE_DIR}") done -echo "Done!" +# Assemble the full manifest in one shot by reading each plugin's package.json. The optional "loader" field is +# preserved as-is so plugin authors cannot shadow core fields like name/version/main. +"${NODE_EXE:-node}" -e ' + const fs = require("fs"); + const path = require("path"); + const plugins = process.argv.slice(1).map((dir) => { + const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")); + const entry = { name: pkg.name, version: pkg.version, main: pkg.main }; + if (pkg.loader != null) { + entry.loader = pkg.loader; + } + return entry; + }); + process.stdout.write(JSON.stringify({ plugins }, null, 2)); +' ${PACKAGE_DIRS[@]+"${PACKAGE_DIRS[@]}"} > "${MANIFEST_FILE}" -# Close out the manifest file -echo "]}" >> "${MANIFEST_FILE}" +echo "Done!" diff --git a/plugin/src/main/java/io/deephaven/plugin/js/JsPlugin.java b/plugin/src/main/java/io/deephaven/plugin/js/JsPlugin.java index be60667c826..bc6fbe78a8d 100644 --- a/plugin/src/main/java/io/deephaven/plugin/js/JsPlugin.java +++ b/plugin/src/main/java/io/deephaven/plugin/js/JsPlugin.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; /** * A JS plugin is a {@link Plugin} that allows for custom javascript and related content to be served, see @@ -100,6 +101,15 @@ public Paths paths() { return Paths.all(); } + /** + * The JS plugin loader configuration, as sourced from the "loader" field of the plugin's package.json (if present). + * This is an arbitrary, plugin-specific JSON object that is opaque to the server and is simply passed through as + * the "loader" field for the manifest entry in "js-plugins/manifest.json". + * + * @return the loader configuration + */ + public abstract Optional loader(); + @Override public final > T walk(V visitor) { return visitor.visit(this); @@ -142,6 +152,8 @@ public interface Builder { Builder paths(Paths paths); + Builder loader(Object loader); + JsPlugin build(); } } diff --git a/py/server/deephaven/__init__.py b/py/server/deephaven/__init__.py index 98b89d1011f..93d0495e3e7 100644 --- a/py/server/deephaven/__init__.py +++ b/py/server/deephaven/__init__.py @@ -7,6 +7,11 @@ """ +# The deephaven package is split across multiple distributions (e.g. deephaven-core and deephaven-plugin); extend the +# search path so subpackages installed elsewhere (such as deephaven.plugin) remain importable when this copy shadows +# site-packages. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + import importlib.metadata from deephaven_internal import jvm diff --git a/py/server/deephaven/jcompat.py b/py/server/deephaven/jcompat.py index f332a2f8e87..01b9c82ead7 100644 --- a/py/server/deephaven/jcompat.py +++ b/py/server/deephaven/jcompat.py @@ -49,6 +49,8 @@ "io.deephaven.integrations.common.PrimitiveArrayConversionUtility" ) _JTableDefinition = jpy.get_type("io.deephaven.engine.table.TableDefinition") +_JLinkedHashMap = jpy.get_type("java.util.LinkedHashMap") +_JArrayList = jpy.get_type("java.util.ArrayList") _DH_PANDAS_NULLABLE_TYPE_MAP: dict[DType, type[ExtensionDtype]] = { bool_: pd.BooleanDtype, @@ -90,6 +92,42 @@ def j_hashmap(d: Optional[dict] = None) -> Optional[jpy.JType]: return r +JsonValue = Union[ + None, + bool, + int, + float, + str, + dict[Any, "JsonValue"], + list["JsonValue"], + tuple["JsonValue", ...], +] +"""A JSON-compatible Python value: a scalar, or a dict/list/tuple of them.""" + + +def j_json_value(value: JsonValue) -> Union[jpy.JType, None, bool, int, float, str]: + """Recursively converts a JSON-compatible Python value into Java objects. + + A dict is converted to a java.util.LinkedHashMap and a list or tuple is converted to a + java.util.ArrayList, with their keys and values converted recursively. Any other value is + unwrapped and left for jpy to convert. + + Nested containers must be converted explicitly; otherwise jpy passes them through as opaque + org.jpy.PyObject values. + """ + if isinstance(value, dict): + r = _JLinkedHashMap(len(value)) + for k, v in value.items(): + r.put(j_json_value(k), j_json_value(v)) + return r + if isinstance(value, (list, tuple)): + r = _JArrayList(len(value)) + for v in value: + r.add(j_json_value(v)) + return r + return unwrap(value) + + def j_hashset(s: Optional[Union[set, Sequence]] = None) -> Optional[jpy.JType]: """Creates a Java HashSet from a set.""" if s is None: diff --git a/py/server/deephaven_internal/plugin/js/__init__.py b/py/server/deephaven_internal/plugin/js/__init__.py index a821bdbb692..486ba1ae347 100644 --- a/py/server/deephaven_internal/plugin/js/__init__.py +++ b/py/server/deephaven_internal/plugin/js/__init__.py @@ -5,6 +5,7 @@ import pathlib import jpy +from deephaven.jcompat import j_json_value from deephaven.plugin.js import JsPlugin _JJsPlugin = jpy.get_type("io.deephaven.plugin.js.JsPlugin") @@ -26,4 +27,10 @@ def to_j_js_plugin(js_plugin: JsPlugin) -> jpy.JType: builder.version(js_plugin.version) builder.main(main_path) builder.path(j_path) + # "loader" is not required field on the deephaven.plugin.js.JsPlugin interface, so we duck-type check for it + # here to allow plugins to opt-in to providing loader configuration without requiring a new release of the + # deephaven-plugin package. + loader = getattr(js_plugin, "loader", None) + if loader is not None: + builder.loader(j_json_value(loader)) return builder.build() diff --git a/py/server/tests/test_jcompat.py b/py/server/tests/test_jcompat.py index 161cf4dc0ad..f31239e2f9f 100644 --- a/py/server/tests/test_jcompat.py +++ b/py/server/tests/test_jcompat.py @@ -2,6 +2,7 @@ # Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending # +import json import unittest import jpy @@ -13,11 +14,15 @@ j_collection_to_list, j_function, j_hashset, + j_json_value, j_lambda, ) from tests.testbase import BaseTestCase _JSharedContext = jpy.get_type("io.deephaven.engine.table.SharedContext") +_JMap = jpy.get_type("java.util.Map") +_JList = jpy.get_type("java.util.List") +_JObjectMapper = jpy.get_type("com.fasterxml.jackson.databind.ObjectMapper") class JCompatTestCase(BaseTestCase): @@ -56,6 +61,91 @@ def test_j_collection_to_list(self): j_set = j_hashset(s) self.assertEqual(s, set(j_collection_to_list(j_set))) + def assert_json_equals(self, j_object, expected) -> None: + """Serializes a Java object with Jackson and asserts the resulting JSON matches expected.""" + actual = json.loads(_JObjectMapper().writeValueAsString(j_object)) + self.assertEqual(actual, expected) + + def assert_round_trip(self, value) -> None: + """Asserts that value survives conversion to Java and JSON serialization unchanged.""" + self.assert_json_equals(j_json_value(value), value) + + def test_j_json_value_basic_values_pass_through(self): + for value in [None, True, False, 0, 42, -7, 0.0, 1.5, -2.25, "", "a string"]: + with self.subTest(value=value): + self.assertIs(j_json_value(value), value) + + def test_j_json_value_dict(self): + j_map = j_json_value({"key": "value", "count": 3}) + self.assertTrue(_JMap.jclass.isInstance(j_map)) + self.assertEqual(j_map.size(), 2) + self.assertEqual(j_map.get("key"), "value") + self.assertEqual(j_map.get("count"), 3) + + def test_j_json_value_dict_of_basic_values(self): + self.assert_round_trip( + { + "none": None, + "true": True, + "false": False, + "int": 42, + "negative_int": -7, + "float": 1.5, + "str": "a string", + } + ) + + def test_j_json_value_nested_dict(self): + j_map = j_json_value({"outer": {"inner": {"deepest": "value"}}}) + j_outer = j_map.get("outer") + self.assertTrue(_JMap.jclass.isInstance(j_outer)) + j_inner = j_outer.get("inner") + self.assertTrue(_JMap.jclass.isInstance(j_inner)) + self.assertEqual(j_inner.get("deepest"), "value") + + def test_j_json_value_list(self): + j_list = j_json_value(["a", "b", "c"]) + self.assertTrue(_JList.jclass.isInstance(j_list)) + self.assertEqual(j_list.size(), 3) + self.assertEqual(j_list.get(0), "a") + self.assertEqual(j_list.get(2), "c") + + def test_j_json_value_tuple(self): + j_list = j_json_value(("a", "b")) + self.assertTrue(_JList.jclass.isInstance(j_list)) + self.assert_json_equals(j_list, ["a", "b"]) + + def test_j_json_value_empty_containers(self): + self.assert_round_trip({}) + self.assert_round_trip([]) + self.assert_round_trip({"empty_map": {}, "empty_list": []}) + + def test_j_json_value_nested_containers(self): + self.assert_round_trip( + { + "list_of_dicts": [{"a": 1}, {"b": [2, 3]}], + "dict_of_lists": {"x": [[1, 2], []], "y": [{"z": None}]}, + "deep": {"a": {"b": {"c": {"d": ["e", {"f": True}]}}}}, + } + ) + + def test_j_json_value_list_of_lists(self): + j_list = j_json_value([[1, 2], [3]]) + self.assertTrue(_JList.jclass.isInstance(j_list.get(0))) + self.assert_json_equals(j_list, [[1, 2], [3]]) + + def test_j_json_value_non_string_keys(self): + # Jackson serializes non-string map keys as strings, which is the same behavior as json.dumps + self.assert_json_equals( + j_json_value({1: "a", 2.5: "b"}), {"1": "a", "2.5": "b"} + ) + + def test_j_json_value_unwraps_wrapped_objects(self): + j_map = j_json_value( + {"context": AutoCloseable(_JSharedContext.makeSharedContext())} + ) + self.assertTrue(_JSharedContext.jclass.isInstance(j_map.get("context"))) + if __name__ == "__main__": unittest.main() diff --git a/py/server/tests/test_plugin_js.py b/py/server/tests/test_plugin_js.py new file mode 100644 index 00000000000..9ea0813f868 --- /dev/null +++ b/py/server/tests/test_plugin_js.py @@ -0,0 +1,124 @@ +# +# Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending +# + +import json +import pathlib +import tempfile +import unittest + +import jpy +from deephaven.plugin.js import JsPlugin + +from deephaven_internal.plugin.js import to_j_js_plugin +from tests.testbase import BaseTestCase + +_JObjectMapper = jpy.get_type("com.fasterxml.jackson.databind.ObjectMapper") + + +class _MyJsPlugin(JsPlugin): + """A minimal JsPlugin for testing.""" + + def __init__( + self, + path: pathlib.Path, + name: str = "@deephaven_test/example", + version: str = "0.1.0", + main: str = "dist/index.js", + loader=None, + ): + self._path = path + self._name = name + self._version = version + self._main = main + self._loader = loader + + def path(self) -> pathlib.Path: + return self._path + + @property + def name(self) -> str: + return self._name + + @property + def version(self) -> str: + return self._version + + @property + def main(self) -> str: + return self._main + + @property + def loader(self): + return self._loader + + +class _MyLoaderlessJsPlugin: + """A JsPlugin-like object that does not have a "loader" attribute at all, as is the case for plugins written + against older versions of the deephaven-plugin package.""" + + def __init__(self, path: pathlib.Path): + self._path = path + + def path(self) -> pathlib.Path: + return self._path + + name = "@deephaven_test/loaderless" + version = "0.2.0" + main = "dist/index.js" + + +class PluginJsTestCase(BaseTestCase): + def setUp(self) -> None: + super().setUp() + self._tmp_dir = tempfile.TemporaryDirectory() + self.plugin_path = pathlib.Path(self._tmp_dir.name) + (self.plugin_path / "dist").mkdir() + (self.plugin_path / "dist" / "index.js").write_text("// test js plugin") + + def tearDown(self) -> None: + self._tmp_dir.cleanup() + super().tearDown() + + def assert_json_equals(self, j_object, expected) -> None: + """Serializes a Java object with Jackson, the same way the server serializes the loader configuration into + "js-plugins/manifest.json", and asserts the resulting JSON matches expected.""" + actual = json.loads(_JObjectMapper().writeValueAsString(j_object)) + self.assertEqual(actual, expected) + + def test_to_j_js_plugin(self): + j_plugin = to_j_js_plugin(_MyJsPlugin(self.plugin_path)) + self.assertEqual(j_plugin.name(), "@deephaven_test/example") + self.assertEqual(j_plugin.version(), "0.1.0") + self.assertEqual(j_plugin.main().toString(), "dist/index.js") + self.assertEqual(j_plugin.path().toString(), str(self.plugin_path)) + self.assertFalse(j_plugin.loader().isPresent()) + + def test_to_j_js_plugin_with_loader(self): + loader = {"entry": "./dist/loader.js", "config": {"deps": ["a", "b"]}} + j_plugin = to_j_js_plugin(_MyJsPlugin(self.plugin_path, loader=loader)) + self.assertTrue(j_plugin.loader().isPresent()) + self.assert_json_equals(j_plugin.loader().get(), loader) + + def test_to_j_js_plugin_with_basic_loader(self): + for loader in [True, 42, 1.5, "a string", ["a", "b"]]: + with self.subTest(loader=loader): + j_plugin = to_j_js_plugin(_MyJsPlugin(self.plugin_path, loader=loader)) + self.assertTrue(j_plugin.loader().isPresent()) + self.assert_json_equals(j_plugin.loader().get(), loader) + + def test_to_j_js_plugin_without_loader_attribute(self): + j_plugin = to_j_js_plugin(_MyLoaderlessJsPlugin(self.plugin_path)) + self.assertEqual(j_plugin.name(), "@deephaven_test/loaderless") + self.assertFalse(j_plugin.loader().isPresent()) + + def test_to_j_js_plugin_non_path(self): + js_plugin = _MyJsPlugin(self.plugin_path) + js_plugin._path = str(self.plugin_path) + with self.assertRaises(Exception) as cm: + to_j_js_plugin(js_plugin) + self.assertIn("Expecting pathlib.Path", str(cm.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java b/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java index 3e98a86e590..9b16a26894f 100644 --- a/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java +++ b/server/jetty/src/test/java/io/deephaven/server/jetty/JettyFlightRoundTripTest.java @@ -156,14 +156,21 @@ private void testJsPluginExamples(boolean example1IsLimited, boolean example2IsL private void manifestTest12(HttpClient client) throws InterruptedException, TimeoutException, ExecutionException { final ContentResponse manifestResponse = get(client, "js-plugins/manifest.json"); - assertOk(manifestResponse, "application/json", - "{\"plugins\":[{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\"}]}"); + assertOk(manifestResponse, "application/json", """ + {"plugins":[\ + {"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js","loader":{"npm":"loader"}},\ + {"name":"@deephaven_test/example2","version":"0.2.0","main":"dist/index.js"}\ + ]}"""); } private void manifestTest123(HttpClient client) throws InterruptedException, TimeoutException, ExecutionException { final ContentResponse manifestResponse = get(client, "js-plugins/manifest.json"); - assertOk(manifestResponse, "application/json", - "{\"plugins\":[{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\"},{\"name\":\"@deephaven_test/example3\",\"version\":\"0.3.0\",\"main\":\"index.js\"}]}"); + assertOk(manifestResponse, "application/json", """ + {"plugins":[\ + {"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js","loader":{"foo":"bar"}},\ + {"name":"@deephaven_test/example2","version":"0.2.0","main":"dist/index.js"},\ + {"name":"@deephaven_test/example3","version":"0.3.0","main":"index.js"}\ + ]}"""); } private void example1Tests(HttpClient client, boolean isLimited) @@ -173,8 +180,9 @@ private void example1Tests(HttpClient client, boolean isLimited) .isEqualTo(HttpStatus.NOT_FOUND_404); } else { assertOk(get(client, "js-plugins/@deephaven_test/example1/package.json"), - "application/json", - "{\"name\":\"@deephaven_test/example1\",\"version\":\"0.1.0\",\"main\":\"dist/index.js\",\"files\":[\"dist\"]}"); + "application/json", """ + {"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js",\ + "files":["dist"],"loader":{"npm":"loader"}}"""); } assertOk( @@ -195,8 +203,9 @@ private void example2Tests(HttpClient client, boolean isLimited) .isEqualTo(HttpStatus.NOT_FOUND_404); } else { assertOk(get(client, "js-plugins/@deephaven_test/example2/package.json"), - "application/json", - "{\"name\":\"@deephaven_test/example2\",\"version\":\"0.2.0\",\"main\":\"dist/index.js\",\"files\":[\"dist\"]}"); + "application/json", """ + {"name":"@deephaven_test/example2","version":"0.2.0","main":"dist/index.js",\ + "files":["dist"]}"""); } assertOk( diff --git a/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java index d46da3afb7c..02745d9b01d 100644 --- a/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java +++ b/server/jetty/src/test/java/io/deephaven/server/jetty/js/Example123Registration.java @@ -9,6 +9,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; +import java.util.Map; public final class Example123Registration implements Registration { @@ -39,6 +40,7 @@ private static JsPlugin example1() throws URISyntaxException { .version("0.1.0") .main(main) .path(resourcePath) + .loader(Map.of("foo", "bar")) .build(); } diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json index b3733e4fe6d..aaee38d0166 100644 --- a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json +++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/@deephaven_test/example1/package.json @@ -1 +1 @@ -{"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js","files":["dist"]} \ No newline at end of file +{"name":"@deephaven_test/example1","version":"0.1.0","main":"dist/index.js","files":["dist"],"loader":{"npm":"loader"}} \ No newline at end of file diff --git a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json index eaa745d1b6b..a11dd3a40ed 100644 --- a/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json +++ b/server/jetty/src/test/resources/io/deephaven/server/jetty/js/examples/manifest.json @@ -3,7 +3,10 @@ { "name": "@deephaven_test/example1", "main": "dist/index.js", - "version": "0.1.0" + "version": "0.1.0", + "loader": { + "foo": "bar" + } }, { "name": "@deephaven_test/example2", diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java index 30cdd193d1a..992db33cd3b 100644 --- a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java +++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginFromNpmPackage.java @@ -35,6 +35,9 @@ static JsPlugin of(Path packageRoot) throws IOException { .main(main) .path(packageRoot) .paths(paths); + if (packageJson.loader() != null) { + builder.loader(packageJson.loader()); + } return builder.build(); } } diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java index 5f1963de04f..5074d37d6f2 100644 --- a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java +++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginManifestEntry.java @@ -4,53 +4,96 @@ package io.deephaven.server.plugin.js; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import io.deephaven.annotations.SimpleStyle; +import io.deephaven.annotations.BuildableStyle; import io.deephaven.plugin.js.JsPlugin; import org.immutables.value.Value.Immutable; -import org.immutables.value.Value.Parameter; +import org.jetbrains.annotations.Nullable; /** * @see js-plugin-template */ @Immutable -@SimpleStyle +@BuildableStyle +@JsonInclude(JsonInclude.Include.NON_NULL) public abstract class JsPluginManifestEntry { public static final String NAME = "name"; public static final String VERSION = "version"; public static final String MAIN = "main"; + public static final String LOADER = "loader"; + + public static Builder builder() { + return ImmutableJsPluginManifestEntry.builder(); + } - @JsonCreator public static JsPluginManifestEntry of( @JsonProperty(value = NAME, required = true) String name, @JsonProperty(value = VERSION, required = true) String version, @JsonProperty(value = MAIN, required = true) String main) { - return ImmutableJsPluginManifestEntry.of(name, version, main); + return JsPluginManifestEntry.of(name, version, main, null); + } + + @JsonCreator + public static JsPluginManifestEntry of( + @JsonProperty(value = NAME, required = true) String name, + @JsonProperty(value = VERSION, required = true) String version, + @JsonProperty(value = MAIN, required = true) String main, + @JsonProperty(value = LOADER) Object loader) { + final Builder builder = builder() + .name(name) + .version(version) + .main(main); + if (loader != null) { + builder.loader(loader); + } + return builder.build(); } public static JsPluginManifestEntry from(JsPlugin plugin) { - return of(plugin.name(), plugin.version(), plugin.main().toString()); + final Builder builder = builder() + .name(plugin.name()) + .version(plugin.version()) + .main(plugin.main().toString()); + plugin.loader().ifPresent(builder::loader); + return builder.build(); } /** * The name of the plugin. */ - @Parameter @JsonProperty(NAME) public abstract String name(); /** * The version of the plugin. */ - @Parameter @JsonProperty(VERSION) public abstract String version(); /** * The main js file. */ - @Parameter @JsonProperty(MAIN) public abstract String main(); + + /** + * The optional loader configuration, sourced from the plugin's "loader" package.json field (if present). + */ + @Nullable + @JsonProperty(LOADER) + public abstract Object loader(); + + public interface Builder { + Builder name(String name); + + Builder version(String version); + + Builder main(String main); + + Builder loader(Object loader); + + JsPluginManifestEntry build(); + } } diff --git a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginsFromManifest.java b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginsFromManifest.java index 3eaa52854a2..84061dbd8a9 100644 --- a/server/src/main/java/io/deephaven/server/plugin/js/JsPluginsFromManifest.java +++ b/server/src/main/java/io/deephaven/server/plugin/js/JsPluginsFromManifest.java @@ -18,15 +18,17 @@ static List of(Path manifestRoot) throws IOException { for (JsPluginManifestEntry entry : manifest.plugins()) { final Path pluginPath = manifestRoot.resolve(entry.name()); final Path pluginMain = pluginPath.relativize(pluginPath.resolve(entry.main())); - final JsPlugin plugin = JsPlugin.builder() + final JsPlugin.Builder builder = JsPlugin.builder() .name(entry.name()) .version(entry.version()) .main(pluginMain) - .path(pluginPath) - .build(); + .path(pluginPath); + if (entry.loader() != null) { + builder.loader(entry.loader()); + } // We expect manifests to be "production" use cases - they should already be packed as appropriate. // Additionally, there is no strict requirement that they have package.json anyways. - plugins.add(plugin); + plugins.add(builder.build()); } return plugins; } diff --git a/server/src/main/java/io/deephaven/server/plugin/js/NpmPackage.java b/server/src/main/java/io/deephaven/server/plugin/js/NpmPackage.java index 82e2f85094c..f57288e9b24 100644 --- a/server/src/main/java/io/deephaven/server/plugin/js/NpmPackage.java +++ b/server/src/main/java/io/deephaven/server/plugin/js/NpmPackage.java @@ -5,9 +5,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import io.deephaven.annotations.SimpleStyle; +import io.deephaven.annotations.BuildableStyle; import org.immutables.value.Value.Immutable; -import org.immutables.value.Value.Parameter; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; @@ -17,15 +17,29 @@ import static io.deephaven.server.plugin.js.Jackson.OBJECT_MAPPER; @Immutable -@SimpleStyle +@BuildableStyle abstract class NpmPackage { + static final String LOADER = "loader"; + + public static Builder builder() { + return ImmutableNpmPackage.builder(); + } + @JsonCreator public static NpmPackage of( @JsonProperty(value = JsPluginNpmPackageRegistration.NAME, required = true) String name, @JsonProperty(value = JsPluginNpmPackageRegistration.VERSION, required = true) String version, - @JsonProperty(value = JsPluginNpmPackageRegistration.MAIN, required = true) String main) { - return ImmutableNpmPackage.of(name, version, main); + @JsonProperty(value = JsPluginNpmPackageRegistration.MAIN, required = true) String main, + @JsonProperty(value = LOADER) Object loader) { + final Builder builder = builder() + .name(name) + .version(version) + .main(main); + if (loader != null) { + builder.loader(loader); + } + return builder.build(); } public static NpmPackage read(Path packageJson) throws IOException { @@ -35,15 +49,31 @@ public static NpmPackage read(Path packageJson) throws IOException { } } - @Parameter @JsonProperty(JsPluginNpmPackageRegistration.NAME) public abstract String name(); - @Parameter @JsonProperty(JsPluginNpmPackageRegistration.VERSION) public abstract String version(); - @Parameter @JsonProperty(JsPluginNpmPackageRegistration.MAIN) public abstract String main(); + + /** + * The optional "loader" field, an arbitrary plugin-specific JSON value. + */ + @Nullable + @JsonProperty(LOADER) + public abstract Object loader(); + + public interface Builder { + Builder name(String name); + + Builder version(String version); + + Builder main(String main); + + Builder loader(Object loader); + + NpmPackage build(); + } }