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

benweidig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit cd3518fea9fe6a2ef02e410af9a78ab11ca68384
Author: Ben Weidig <[email protected]>
AuthorDate: Sat Apr 18 11:23:33 2026 +0200

    TAP5-2809: NpmTask with Docker support, don't emit TypeScript types into 
es-modules
    
    As it's not guaranteed that npm is available, and to make builds more
    stable/reproducible, an NpmTask was added that checks for Docker first
    to run npm with a fallback to a local installation.
    
    The existing npm/typescript related code in tapestr-core build.gradle
    were adapted.
    
    Furthermore, the TypeScript project package.json/tsconfig.json was
    slightly improved, no longer emitting the typings into the es-modules,
    and hardening script calling in general.
---
 buildSrc/src/main/groovy/t5build/NpmTask.groovy | 109 ++++++++++++++++++++++++
 tapestry-core/build.gradle                      |  39 +++------
 tapestry-core/src/main/typescript/package.json  |  22 ++---
 tapestry-core/src/main/typescript/tsconfig.json |   2 -
 4 files changed, 134 insertions(+), 38 deletions(-)

diff --git a/buildSrc/src/main/groovy/t5build/NpmTask.groovy 
b/buildSrc/src/main/groovy/t5build/NpmTask.groovy
new file mode 100644
index 000000000..8786d9745
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/NpmTask.groovy
@@ -0,0 +1,109 @@
+// Licensed 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.
+
+package t5build
+
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.Internal
+
+/**
+ * Gradle task that runs an npm command, either via Docker or the local npm 
executable.
+ * <p>
+ * When Docker is available, the task mounts the project's {@code src/main} 
directory into
+ * a container and runs {@cpde npm} there, ensuring a consistent Node.js 
environment.
+ * <p>
+ * On Linux, the container process runs as the current host user to avoid 
file-ownership
+ * issues on mounted volumes.
+ * <p>
+ * When Docker is not available, the task falls back to invoking the local 
{@code npm} (or
+ * {@code npm.cmd} on Windows) directly.
+ * <p>
+ * The Docker image used can be overridden via the {@code nodeDockerImage} 
Gradle project
+ * property (defaults to {@code node:lts-alpine}).
+ * <p>
+ * Usage in a build script:
+ * <pre>
+ * tasks.register('npmInstall', NpmTask) {
+ *     npmArgs = ['install']
+ * }
+ * </pre>
+ */
+class NpmTask extends Exec {
+
+    private static Boolean dockerAvailableCache = null
+
+    /**
+     * Arguments passed to the npm command (e.g. {@code ['install']}).
+     */
+    @Internal
+    List<String> npmArgs = []
+
+    /**
+     * Docker image used to run npm when Docker is available.
+     * <p>
+     * Default: {@code node:lts-alpine}
+     */
+    @Internal
+    String nodeDockerImage = (project.findProperty('nodeDockerImage') ?: 
'node:lts-alpine') as String
+
+    NpmTask() {
+        workingDir = 'src/main/typescript'
+    }
+
+    @Override
+    protected void exec() {
+        commandLine(buildCommand())
+        super.exec()
+    }
+
+    private List<String> buildCommand() {
+        return isDockerAvailable() ? buildDockerCommand() : [npmExecutable()] 
+ npmArgs
+    }
+
+    private List<String> buildDockerCommand() {
+        def cmd = [
+            'docker', 'run', '--rm',
+            '-v', "${project.projectDir}/src/main:/work",
+            '-w', '/work/typescript']
+        if (isLinux()) {
+            cmd += ['--user', "${['id', '-u'].execute().text.trim()}:${['id', 
'-g'].execute().text.trim()}"]
+        }
+        cmd += [nodeDockerImage, 'npm'] + npmArgs
+        return cmd
+    }
+
+    private static boolean isDockerAvailable() {
+        if (dockerAvailableCache == null) {
+            try {
+                def proc = ['docker', 'info'].execute()
+                proc.waitFor()
+                dockerAvailableCache = proc.exitValue() == 0
+            } catch (Exception ignored) {
+                dockerAvailableCache = false
+            }
+        }
+        return dockerAvailableCache
+    }
+
+    private static String npmExecutable() {
+        return isWindows() ? 'npm.cmd' : 'npm'
+    }
+
+    private static boolean isWindows() {
+        return System.properties['os.name'].toLowerCase().contains('windows')
+    }
+
+    private static boolean isLinux() {
+        def os = System.properties['os.name'].toLowerCase()
+        return !os.contains('windows') && !os.contains('mac')
+    }
+}
diff --git a/tapestry-core/build.gradle b/tapestry-core/build.gradle
index 0b0afd063..d4d837b3e 100644
--- a/tapestry-core/build.gradle
+++ b/tapestry-core/build.gradle
@@ -1,3 +1,5 @@
+import t5build.NpmTask
+
 plugins {
     id 'tapestry.junit5-convention'
     id 'tapestry.testng-convention'
@@ -5,12 +7,6 @@ plugins {
 
 description = 'Central module for Tapestry, containing all core services and 
components'
 
-def npmWorkingDir = 'src/main/typescript/'
-def npmExec = 'npm'
-if (System.properties['os.name'].toLowerCase().contains('windows')) {
-    npmExec += '.cmd'
-}
-
 dependencies {
     api project(':tapestry-ioc')
     api project(':tapestry-json')
@@ -29,37 +25,32 @@ dependencies {
     provided libs.jakarta.servlet.api
 
     testImplementation libs.commons.httpclient
-    testImplementation project(":tapestry-spock")
+    testImplementation project(':tapestry-spock')
 
     testRuntimeOnly libs.hsqldb
 }
 
-tasks.register('npmInstall', Exec) {
+tasks.register('npmInstall', NpmTask) {
     group = 'typescript'
-    description = 'Runs npm install'
-    
-    workingDir = npmWorkingDir
-    commandLine npmExec, 'install'
+    description = 'Runs npm ci'
+
+    npmArgs = ['ci']
 }
 
-tasks.register('compileTypeScriptToAmd', Exec) {
+tasks.register('compileTypeScriptToAmd', NpmTask) {
     group = 'typescript'
     description = 'Compiles TypeScript to AMD modules'
-
     dependsOn npmInstall
 
-    workingDir = npmWorkingDir
-    commandLine npmExec, 'run', 'build-amd'
+    npmArgs = ['run', 'build-amd']
 }
 
-tasks.register('compileTypeScriptToEsModule', Exec) {
+tasks.register('compileTypeScriptToEsModule', NpmTask) {
     group = 'typescript'
     description = 'Compiles TypeScript to ES modules'
-
     dependsOn npmInstall
-    
-    workingDir = npmWorkingDir
-    commandLine npmExec, 'run', 'build-es-module'
+
+    npmArgs = ['run', 'build-es-module']
 }
 
 // Lifecycle task that triggers both TypeScript compilations.
@@ -76,14 +67,12 @@ tasks.named('sourcesJar') {
     dependsOn compileTypeScript
 }
 
-tasks.register('generateTypeScriptDocs', Exec) {
+tasks.register('generateTypeScriptDocs', NpmTask) {
     group = 'typescript'
     description = 'Generates TypeScript documentation'
-
     dependsOn npmInstall
 
-    workingDir = npmWorkingDir
-    commandLine npmExec, 'run', 'docs'
+    npmArgs = ['run', 'docs']
 }
 
 tasks.register('cleanTypeScriptFiles', Delete) {
diff --git a/tapestry-core/src/main/typescript/package.json 
b/tapestry-core/src/main/typescript/package.json
index 51d7af186..d13fdc044 100644
--- a/tapestry-core/src/main/typescript/package.json
+++ b/tapestry-core/src/main/typescript/package.json
@@ -10,21 +10,21 @@
   },
   "dependencies": {},
   "scripts": {
-    "build": "npm run build-es-module; npm run build-amd; rm -f .",
-    "build-es-module": "npx tsc --outDir 
../resources/META-INF/assets/es-modules/; npm run adjust-es-module;",
-    "build-amd": "npx tsc --module amd --outDir 
../resources/META-INF/modules/; npm run adjust-amd;",
-    "clean-amd": "rm -rf ../resources/META-INF/modules/t5/core/*.js; rm -rf 
../resources/META-INF/modules/t5/beanvalidation/*.js;",
-    "clean-es-module": "rm -rf 
../resources/META-INF/assets/es-modules/t5/core/*.js; 
../resources/META-INF/assets/es-modules/t5/beanvalidation/*.js;",
-    "clean": "npm run clean-amd; npm run clean-es-module; npm run clean-docs; 
npm run clean-tsc;",
-    "adjust-amd": "mv ../resources/META-INF/modules/t5/core/messages-amd.js 
../resources/META-INF/modules/t5/core/messages.js; rm 
../resources/META-INF/modules/t5/core/messages-es-module.js;",
-    "adjust-es-module": "mv 
../resources/META-INF/assets/es-modules/t5/core/messages-es-module.js 
../resources/META-INF/assets/es-modules/t5/core/messages.js; rm 
../resources/META-INF/assets/es-modules/t5/core/messages-amd.js;",
+    "build": "npm run build-es-module && npm run build-amd",
+    "build-es-module": "tsc --outDir ../resources/META-INF/assets/es-modules/ 
&& npm run adjust-es-module",
+    "build-amd": "tsc --module amd --outDir ../resources/META-INF/modules/ && 
npm run adjust-amd",
+    "clean-amd": "rm -rf ../resources/META-INF/modules/t5/core/*.js && rm -rf 
../resources/META-INF/modules/t5/beanvalidation/*.js",
+    "clean-es-module": "rm -rf 
../resources/META-INF/assets/es-modules/t5/core/*.js && rm -rf 
../resources/META-INF/assets/es-modules/t5/beanvalidation/*.js",
+    "clean": "npm run clean-amd && npm run clean-es-module && npm run 
clean-docs && npm run clean-tsc",
+    "adjust-amd": "mv ../resources/META-INF/modules/t5/core/messages-amd.js 
../resources/META-INF/modules/t5/core/messages.js && rm 
../resources/META-INF/modules/t5/core/messages-es-module.js",
+    "adjust-es-module": "mv 
../resources/META-INF/assets/es-modules/t5/core/messages-es-module.js 
../resources/META-INF/assets/es-modules/t5/core/messages.js && rm 
../resources/META-INF/assets/es-modules/t5/core/messages-amd.js",
     "docs": "typedoc",
     "clean-docs": "rm -rf docs",
     "tsc": "tsc",
     "clean-tsc": "rm -rf ./types/*",
-    "generate-type-definitions": "tsc --emitDeclarationOnly --outDir ./types",
-    "test-publish-to-npm": "npm run clean-tsc; npm run 
generate-type-definitions; npm pack;",
-    "publish-to-npm": "npm run clean-tsc; npm run generate-type-definitions; 
npm publish --access public;"
+    "generate-type-definitions": "tsc --declaration --emitDeclarationOnly 
--outDir ./types",
+    "test-publish-to-npm": "npm run clean-tsc && npm run 
generate-type-definitions && npm pack",
+    "publish-to-npm": "npm run clean-tsc && npm run generate-type-definitions 
&& npm publish --access public"
   },
   "license": "Apache-2.0",
   "name": "@thiagohp/tapestry-5",
diff --git a/tapestry-core/src/main/typescript/tsconfig.json 
b/tapestry-core/src/main/typescript/tsconfig.json
index 33aa9ddde..bd501e030 100644
--- a/tapestry-core/src/main/typescript/tsconfig.json
+++ b/tapestry-core/src/main/typescript/tsconfig.json
@@ -34,8 +34,6 @@
     // "skipDefaultLibCheck": true,                      /* Skip type checking 
.d.ts files that are included with TypeScript. */
     "skipLibCheck": true                                 /* Skip type checking 
all .d.ts files. */,
 
-    "declaration": true,
-
     "outDir": "output",
     "rootDir": "src",
 

Reply via email to