This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 255e9e7b85e build: decouple JS API client bundling into an optional 
module (#4603)
255e9e7b85e is described below

commit 255e9e7b85efb801f4dad1c557aaa4ff5d6c4e0e
Author: David Smiley <[email protected]>
AuthorDate: Fri Jul 10 23:21:17 2026 -0400

    build: decouple JS API client bundling into an optional module (#4603)
    
    The OpenAPI-generated JS client (npm install + babel build + browserify 
bundle)
    was embedded directly in :solr:webapp's build.gradle, making that module 
quietly
    depend on npm/node for something unrelated to compiling/packaging the webapp
    itself. Pull it out into its own module, :solr:webapp:js-client, which 
exposes
    the built bundle via a `jsClientBundle` configuration that :solr:webapp now
    consumes as a normal cross-project dependency (mirroring how it already 
consumes
    :solr:server's serverLib/solrCore and :solr:api's jsClient outputs), 
instead of
    :solr:webapp defining the npm tasks and reaching into its own local task 
outputs.
    
    Also make the JS client bundle optional, mirroring the existing 
disableUiModule
    pattern:
    - settings.gradle: new -PdisableJsClient=true flag 
(gradle.ext.withJsClient),
      excluding :solr:webapp:js-client from the build entirely when set.
    - solr/webapp/build.gradle: the generatedJSClientBundle dependency is only 
added
      when the flag is on; the war{} block's 
from(configurations.generatedJSClientBundle)
      is left unconditional since an empty configuration is already a safe 
no-op.
    
    Refactor gradle/node.gradle so it no longer hardcodes which projects need
    node/npm.
---
 build.gradle                           |   1 +
 gradle/node.gradle                     |  67 ++++++++++---------
 settings.gradle                        |   6 ++
 solr/solr-ref-guide/build.gradle       |   1 +
 solr/webapp/build.gradle               |  91 ++-----------------------
 solr/webapp/gradle.lockfile            |   2 +-
 solr/webapp/js-client/build.gradle.kts | 119 +++++++++++++++++++++++++++++++++
 solr/webapp/js-client/gradle.lockfile  |   5 ++
 8 files changed, 174 insertions(+), 118 deletions(-)

diff --git a/build.gradle b/build.gradle
index dd939aac66a..80c97fe1a0f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -132,6 +132,7 @@ ext {
 
   // Use gradle flag in root project for further referencing
   withUiModule = gradle.ext.withUiModule
+  withJsClient = gradle.ext.withJsClient
 }
 
 // Include smaller chunks configuring dedicated build areas.
diff --git a/gradle/node.gradle b/gradle/node.gradle
index 0cf7923bac0..5934db87fdd 100644
--- a/gradle/node.gradle
+++ b/gradle/node.gradle
@@ -15,47 +15,50 @@
  * limitations under the License.
  */
 
-configure([project(":solr:solr-ref-guide"), project(":solr:webapp")]) {
-  apply plugin: libs.plugins.nodegradle.node.get().pluginId
+// Projects that need node/npm apply the node-gradle plugin themselves; this 
shared
+// config reacts to whichever projects do so, rather than naming them 
explicitly.
+allprojects {
+  pluginManager.withPlugin(libs.plugins.nodegradle.node.get().pluginId) {
 
-  def npmRegistry = "${-> propertyOrEnvOrDefault("solr.npm.registry", 
"SOLR_NPM_REGISTRY", '')}"
-  if (!npmRegistry.isEmpty()) {
-    tasks.npmSetup {
-      args.addAll(['--registry', npmRegistry])
-    }
-
-    afterEvaluate {
-      tasks.withType(NpmTask).each {npmTask ->
-        npmTask.environment.put('NPM_CONFIG_REGISTRY', npmRegistry)
+    def npmRegistry = "${-> propertyOrEnvOrDefault("solr.npm.registry", 
"SOLR_NPM_REGISTRY", '')}"
+    if (!npmRegistry.isEmpty()) {
+      tasks.npmSetup {
+        args.addAll(['--registry', npmRegistry])
       }
-      tasks.withType(NpxTask).each {npxTask ->
-        npxTask.environment.put('NPM_CONFIG_REGISTRY', npmRegistry)
+
+      afterEvaluate {
+        tasks.withType(NpmTask).each {npmTask ->
+          npmTask.environment.put('NPM_CONFIG_REGISTRY', npmRegistry)
+        }
+        tasks.withType(NpxTask).each {npxTask ->
+          npxTask.environment.put('NPM_CONFIG_REGISTRY', npmRegistry)
+        }
       }
     }
-  }
 
-  project.ext {
-    nodeProjectDir = layout.projectDirectory.dir(".gradle/node")
-  }
+    project.ext {
+      nodeProjectDir = layout.projectDirectory.dir(".gradle/node")
+    }
 
-  node {
-    download = true
-    version = libs.versions.nodejs.get()
+    node {
+      download = true
+      version = libs.versions.nodejs.get()
 
-    def nodeDistUrl = "${-> propertyOrEnvOrDefault("solr.node.distUrl", 
"SOLR_NODE_DIST_URL", '')}"
-    if (!nodeDistUrl.isEmpty()) {
-      distBaseUrl = nodeDistUrl
-    }
+      def nodeDistUrl = "${-> propertyOrEnvOrDefault("solr.node.distUrl", 
"SOLR_NODE_DIST_URL", '')}"
+      if (!nodeDistUrl.isEmpty()) {
+        distBaseUrl = nodeDistUrl
+      }
 
-    // The directory where Node.js is unpacked (when download is true)
-    workDir = file("${project.ext.nodeProjectDir.getAsFile().path}/nodejs")
+      // The directory where Node.js is unpacked (when download is true)
+      workDir = file("${project.ext.nodeProjectDir.getAsFile().path}/nodejs")
 
-    // The directory where npm is installed (when a specific version is 
defined)
-    npmWorkDir = file("${project.ext.nodeProjectDir.getAsFile().path}/npm")
+      // The directory where npm is installed (when a specific version is 
defined)
+      npmWorkDir = file("${project.ext.nodeProjectDir.getAsFile().path}/npm")
 
-    // The Node.js project directory location
-    // This is where the package.json file and node_modules directory are 
located
-    // By default it is at the root of the current project
-    nodeProjectDir = project.ext.nodeProjectDir
+      // The Node.js project directory location
+      // This is where the package.json file and node_modules directory are 
located
+      // By default it is at the root of the current project
+      nodeProjectDir = project.ext.nodeProjectDir
+    }
   }
 }
diff --git a/settings.gradle b/settings.gradle
index 76938b2e2cd..be01caa7fd7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -89,5 +89,11 @@ if (gradle.ext.withUiModule) {
   include(":solr:ui")
 }
 
+def disableJsClientValue = providers.gradleProperty('disableJsClient').orNull
+gradle.ext.withJsClient = disableJsClientValue == null || disableJsClientValue 
!= 'true'
+if (gradle.ext.withJsClient) {
+  include(":solr:webapp:js-client")
+}
+
 // Configures development for joint Lucene/ Solr composite build.
 apply from: file('gradle/lucene-dev/lucene-dev-repo-composite.gradle')
diff --git a/solr/solr-ref-guide/build.gradle b/solr/solr-ref-guide/build.gradle
index 66f4437249b..cf95613fecb 100644
--- a/solr/solr-ref-guide/build.gradle
+++ b/solr/solr-ref-guide/build.gradle
@@ -19,6 +19,7 @@ import groovy.json.JsonOutput
 import org.apache.tools.ant.util.TeeOutputStream
 
 apply plugin: 'java'
+apply plugin: libs.plugins.nodegradle.node.get().pluginId
 
 description = 'Solr Reference Guide'
 
diff --git a/solr/webapp/build.gradle b/solr/webapp/build.gradle
index 62d6a130562..6ca3fcc6175 100644
--- a/solr/webapp/build.gradle
+++ b/solr/webapp/build.gradle
@@ -26,14 +26,10 @@ configurations {
   war {}
   serverLib
   solrCore
-  generatedJSClient
   generatedJSClientBundle
-}
-
-ext {
-  jsClientWorkspace = layout.buildDirectory.dir("jsClientWorkspace").get()
-  jsClientBuildDir = layout.buildDirectory.dir("jsClientBuild").get()
-  jsClientBundleDir = layout.buildDirectory.dir("jsClientBundle").get()
+  // TODO: :solr:ui's dev/prod distributions are pulled in via raw 
cross-project task
+  // references (see generateUiDevFiles/generateUiProdFiles below) instead of a
+  // configuration like this one; consider exposing them as a real 
configuration too.
 }
 
 dependencies {
@@ -42,57 +38,11 @@ dependencies {
   solrCore project(":solr:core")
   implementation(configurations.solrCore - configurations.serverLib)
 
-  generatedJSClient project(path: ":solr:api", configuration: "jsClient")
-  generatedJSClientBundle files(jsClientBundleDir) {
-    builtBy "finalizeJsBundleDir"
-  }
-}
-
-task syncJSClientSourceCode(type: Sync) {
-  group = 'Solr JS Client'
-  from configurations.generatedJSClient
-
-  into jsClientWorkspace
-
-  // Keep the node modules, so that they don't need to be re-downloaded
-  preserve {
-    include "node_modules/**"
+  if (gradle.ext.withJsClient) {
+    generatedJSClientBundle project(path: ":solr:webapp:js-client", 
configuration: "jsClientBundle")
   }
 }
 
-task jsClientDownloadDeps(type: NpmTask) {
-  group = 'Solr JS Client'
-  dependsOn tasks.syncJSClientSourceCode
-
-  args = ["install"]
-  workingDir = file(jsClientWorkspace)
-
-  inputs.dir("${jsClientWorkspace}/src")
-  inputs.file("${jsClientWorkspace}/package.json")
-  outputs.dir("${jsClientWorkspace}/node_modules")
-}
-
-task jsClientBuild(type: NpmTask) {
-  group = 'Solr JS Client'
-  dependsOn tasks.jsClientDownloadDeps
-
-  args = ["run", "build"]
-  workingDir = file(jsClientWorkspace)
-
-  inputs.dir("${jsClientWorkspace}/src")
-  inputs.file("${jsClientWorkspace}/package.json")
-  inputs.dir("${jsClientWorkspace}/node_modules")
-  outputs.dir("${jsClientWorkspace}/dist")
-}
-
-task downloadBrowserify(type: NpmTask) {
-  group = 'Build Dependency Download'
-  args = ["install", "browserify@${libs.versions.browserify.get()}"]
-
-  inputs.property("browserify version", libs.versions.browserify.get())
-  outputs.dir("${nodeProjectDir}/node_modules/browserify")
-}
-
 if (gradle.ext.withUiModule) {
   tasks.register("generateUiDevFiles") {
     description = "Generate new UI for development and add files to outputs 
for later referencing."
@@ -113,39 +63,10 @@ if (gradle.ext.withUiModule) {
   }
 }
 
-task generateJsClientBundle(type: NpxTask) {
-  group = 'Solr JS Client'
-  dependsOn tasks.downloadBrowserify
-  dependsOn tasks.jsClientBuild
-
-  command = 'browserify'
-  args = ['dist/index.js', '-s', 'solrApi', '-o', 
"${jsClientBuildDir}/index.js"]
-  workingDir = file(jsClientWorkspace)
-
-  inputs.dir(jsClientWorkspace)
-  inputs.property("browserify version", libs.versions.browserify.get())
-
-  outputs.file("${jsClientBuildDir}/index.js")
-}
-
-task finalizeJsBundleDir(type: Sync) {
-  group = 'Solr JS Client'
-
-  from configurations.generatedJSClient {
-    include "README.md"
-    include "docs/**"
-  }
-
-  from tasks.generateJsClientBundle {
-    include "index.js"
-  }
-
-  into jsClientBundleDir
-}
-
 war {
   from("web")
 
+  // note: nonetheless may be disabled, copying nothing
   from(configurations.generatedJSClientBundle, {
     into "libs/solr"
   })
diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile
index 5a024c3cdc6..50e9b9ad8ec 100644
--- a/solr/webapp/gradle.lockfile
+++ b/solr/webapp/gradle.lockfile
@@ -161,4 +161,4 @@ org.slf4j:jcl-over-slf4j:2.0.17=serverLib,solrCore
 org.slf4j:jul-to-slf4j:2.0.17=serverLib
 org.slf4j:slf4j-api:2.0.17=serverLib,solrCore
 org.xerial.snappy:snappy-java:1.1.10.8=solrCore
-empty=compileClasspath,generatedJSClient,generatedJSClientBundle,jarValidation,missingdoclet,providedCompile,providedRuntime,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,war
+empty=compileClasspath,generatedJSClientBundle,jarValidation,missingdoclet,providedCompile,providedRuntime,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,war
diff --git a/solr/webapp/js-client/build.gradle.kts 
b/solr/webapp/js-client/build.gradle.kts
new file mode 100644
index 00000000000..1ce43c25978
--- /dev/null
+++ b/solr/webapp/js-client/build.gradle.kts
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.github.gradle.node.npm.task.NpmTask
+import com.github.gradle.node.npm.task.NpxTask
+
+// Builds the OpenAPI-generated JS client (from :solr:api) into a single 
bundled
+// file, for :solr:webapp to include in the war. This is the only place in the
+// build that needs npm/node for this purpose; it can be disabled entirely via
+// -PdisableJsClient=true (see settings.gradle).
+
+plugins {
+  id("base")
+  alias(libs.plugins.nodegradle.node)
+}
+
+description = "Generates a JavaScript client for Solr OpenApi"
+
+val jsClientWorkspace = 
layout.buildDirectory.dir("jsClientWorkspace").get().asFile
+val jsClientBuildDir = layout.buildDirectory.dir("jsClientBuild").get().asFile
+val jsClientBundleDir = 
layout.buildDirectory.dir("jsClientBundle").get().asFile
+
+val generatedJSClient = configurations.create("generatedJSClient")
+val jsClientBundle = configurations.create("jsClientBundle") {
+  isCanBeConsumed = true
+  isCanBeResolved = false
+}
+
+dependencies {
+  generatedJSClient(project(path = ":solr:api", configuration = "jsClient"))
+}
+
+val syncJSClientSourceCode = tasks.register<Sync>("syncJSClientSourceCode") {
+  from(generatedJSClient)
+
+  into(jsClientWorkspace)
+
+  // Keep the node modules, so that they don't need to be re-downloaded
+  preserve {
+    include("node_modules/**")
+  }
+}
+
+val jsClientDownloadDeps = tasks.register<NpmTask>("jsClientDownloadDeps") {
+  dependsOn(syncJSClientSourceCode)
+
+  args.set(listOf("install"))
+  workingDir.set(jsClientWorkspace)
+
+  inputs.dir("$jsClientWorkspace/src")
+  inputs.file("$jsClientWorkspace/package.json")
+  outputs.dir("$jsClientWorkspace/node_modules")
+}
+
+val jsClientBuild = tasks.register<NpmTask>("jsClientBuild") {
+  dependsOn(jsClientDownloadDeps)
+
+  args.set(listOf("run", "build"))
+  workingDir.set(jsClientWorkspace)
+
+  inputs.dir("$jsClientWorkspace/src")
+  inputs.file("$jsClientWorkspace/package.json")
+  inputs.dir("$jsClientWorkspace/node_modules")
+  outputs.dir("$jsClientWorkspace/dist")
+}
+
+val downloadBrowserify = tasks.register<NpmTask>("downloadBrowserify") {
+  args.set(listOf("install", "browserify@${libs.versions.browserify.get()}"))
+
+  inputs.property("browserify version", libs.versions.browserify.get())
+  outputs.dir(project.extra["nodeProjectDir"].toString() + 
"/node_modules/browserify")
+}
+
+val generateJsClientBundle = tasks.register<NpxTask>("generateJsClientBundle") 
{
+  dependsOn(downloadBrowserify)
+  dependsOn(jsClientBuild)
+
+  command.set("browserify")
+  args.set(listOf("dist/index.js", "-s", "solrApi", "-o", 
"$jsClientBuildDir/index.js"))
+  workingDir.set(jsClientWorkspace)
+
+  inputs.dir(jsClientWorkspace)
+  inputs.property("browserify version", libs.versions.browserify.get())
+
+  outputs.file("$jsClientBuildDir/index.js")
+}
+
+val finalizeJsBundleDir = tasks.register<Sync>("finalizeJsBundleDir") {
+  from(generatedJSClient) {
+    include("README.md")
+    include("docs/**")
+  }
+
+  from(generateJsClientBundle) {
+    include("index.js")
+  }
+
+  into(jsClientBundleDir)
+}
+
+artifacts {
+  add("jsClientBundle", jsClientBundleDir) {
+    builtBy(finalizeJsBundleDir)
+  }
+}
diff --git a/solr/webapp/js-client/gradle.lockfile 
b/solr/webapp/js-client/gradle.lockfile
new file mode 100644
index 00000000000..3a4ae5a9eaa
--- /dev/null
+++ b/solr/webapp/js-client/gradle.lockfile
@@ -0,0 +1,5 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+# To regenerate this file, run: ./gradlew :solr:webapp:js-client:dependencies 
--write-locks
+empty=generatedJSClient,jarValidation

Reply via email to