Dependencies as DX Modules
This guide provides a streamlined build process that will automate the creation of the EAR file of DX modules and provides the capability to upload it to the WebSphere Application Server.
This Gradle Project includes the following areas of functionality:
- Webpack build script to package JavaScript libraries into a Web Archive (WAR) file
- Replacement of names and labels in WebSphere deployment descriptors.
- Packaging of the WAR file into an Enterprise Application Archive (EAR) file
- Deployment of the DX Module (EAR file) to WebSphere via DXClient
Create a Gradle Project for Common Dependencies
Create a Gradle project to bundle the common dependencies of multiple Script Applications into a DX Module. DX Modules are artifacts that may include Javascript and Styling (CSS, SCSS) files that DX can then inject into the HTML header of the DX Pages. Frontend modules are aggregated by DX and then cached by browsers by default and will deliver performance improvements to the loading sequence of DX pages.
Initial Project Creation
- Create a DxModule/ folder.
-
Add gradle wrapper scripts and library into the DxModule folder.
- Copy files from existing gradle projects. Recreate the folder structure in your new project.
- gradle/wrapper/gradle-wrapper.jar
- gradle/wrapper/gradle-wrapper.properties
- gradlew
- gradlew.bat
- OR install gradle and run gradle wrapper.
- Copy files from existing gradle projects. Recreate the folder structure in your new project.
-
Create submodules folder for each group of dependencies. Use only one submodule folder if it's the requirement for this setup.
Sample:
- React v16 -> DxModule/SubModuleReact16
- React v18 -> DxModule/SubModuleReact18
-
Create a DxModule/gradle.properties file and set the node and npm versions required. Set nodeInstall to false if the existing executables of the current environment is preferred. The gradle project included in this guide is going to be capable of downloading and using its own set of node and npm executables for the build process.
Add the details of the DX deployment target. They will be relayed as parameters whenever the DXClient executable is called by the deploy tasks.nodeInstall=true nodeVersion=16.17.0 npmVersion=8.15.0
Add the folder names for the submodules and Script Applications. Use only one submodule and/or Script Application folder each if it's the requirement for this setup.dxProtocol=https dxHostname=localhost dxPort=10041 dxProfileName=wp_profile
- Single SubModule
# SubModules subModulesCSV=SubModule01 # ScriptApp Projects baseFolder=../ ## last subfolder of path (i.e: subfolder1/subfolder2 --> subfolder2) must be unique to avoid overlapping gradle task names ## verify using ./gradlew tasks scriptAppFoldersCSV=ScriptApp01
- Multiple SubModules
# SubModules subModulesCSV=SubModuleReact16,SubModuleReact18 # ScriptApp Projects baseFolder=../ ## last subfolder of path (i.e: subfolder1/subfolder2 --> subfolder2) must be unique to avoid overlapping gradle task names ## verify using ./gradlew tasks scriptAppFoldersCSV=SampleAppReact16,SampleAppReact18
- Single SubModule
-
Create or update the package.json file for each of the submodule to enumerate the libraries that will be included.
- DxModule/SubModuleReact16/package.json and DxModule/SubModuleReact18/package.json
{ "name": "WebAppDxModule", "version": "1.0.0", "description": "", "main": "modules-index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build-dxsubmodule": "webpack --config webpack.react16.js" }, "author": "", "license": "ISC", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "css-loader": "^6.7.1", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.6.0", "mini-svg-data-uri": "^1.4.4", "terser-webpack-plugin": "^5.3.3", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" }, "overrides": {} }
- Set the appropriate dependencies for the submodule. DxModule/SubModuleReact16/package.json
"dependencies": { "react": "^16.14.0", "react-dom": "^16.14.0" },
- Set the appropriate dependencies for the submodule. DxModule/SubModuleReact18/package.json
"dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" },
- Make sure to set the main file to modules-index.js
"main": "modules-index.js",
- Make sure to add the build-dx-modules among the scripts. DxModule/SubModuleReact16/package.json:
"scripts": { ... "build-dxsubmodule": "webpack --config webpack.react16.js" },
- Make sure to add the build-dx-modules among the scripts. DxModule/SubModuleReact18/package.json:
"scripts": { ... "build-dxsubmodule": "webpack --config webpack.react18.js" },
- DxModule/SubModuleReact16/package.json and DxModule/SubModuleReact18/package.json
-
Add a module-index.js file to each of the submodule. Enumerate the Javascript libraries and components via import and export commands in the file. Provide descriptive aliases for the group of libraries and components that will be included.
-
DxModule/SubModuleReact16/module-index.js
/* Import and Export Main Libraries */ import * as React16 from 'react'; import * as ReactDOM16 from 'react-dom'; export const ReactV16 = { React: React16, ReactDOM: ReactDOM16 }; /* Export index file for the styles (added here to be able to share the filename of JS bundle's alias in Webpack entry config). */ export * from './styles-index.css'
-
DxModule/SubModuleReact18/module-index.js
/* Import and Export Main Libraries */ import * as React18 from 'react'; import * as ReactDOM18 from 'react-dom'; export const ReactV18 = { React: React18, ReactDOM: ReactDOM18 }; /* Export index file for the styles (added here to be able to share the filename of JS bundle's alias in Webpack entry config). */ export * from './styles-index.css'
-
-
Add a styles-index.css file to each of the submodule. Enumerate via import statements the component styling (CSS, SCSS, etc.) required that are not yet imported by any JS nor HTML files in the modules-index.js. Leave it empty if no imports are needed at the moment.
- DxModule/SubModuleReact16/styles-index.css and DxModule/SubModuleReact16/styles-index.css
/* sample import: * @import 'smart-webcomponents-react/source/styles/smart.default.css'; */
- DxModule/SubModuleReact16/styles-index.css and DxModule/SubModuleReact16/styles-index.css
- Add a DxModule/settings.gradle file. Set the value of the rootProject.name. It will be used as the name of the DX Module (war file, ear file, etc.) and the names and ids in the deployment descriptors (web context path, etc.).
rootProject.name = 'React16AndReact18'
- Add a tsconfig.json file to each of the submodules:
- DxModule/SubModuleReact16/tsconfig.json and DxModule/SubModuleReact18/tsconfig.json
{ "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "allowJs": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true } }
Add a Gradle Build Script
- Add a DxModule/build.gradle file with the following content:
plugins { id 'ear' id 'war' id "com.github.node-gradle.node" version "3.4.0" } OperatingSystem os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem() Architecture arch = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentArchitecture() def dxClientExec='dxclient' if (os.windows) { dxClientExec='dxclient.bat' } group 'com.company.dx.module.custom' //version '1.0-SNAPSHOT' sourceCompatibility=11 targetCompatibility=11 repositories { mavenLocal() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { deploy files(war) } // to make sure old files are not going to mix in the bundle task deleteDistDxModule{ delete(file("dist-dx-module")) } def subModules = project.getProperty("subModulesCSV").split(",") def allSubModulesTasks = [tasks.named("deleteDistDxModule")] subModules.each{ subModule -> def appName = subModule.trim().replaceAll('-','').replaceAll('_',''); allSubModulesTasks.add ( tasks.create( 'npmInstall'+appName, NpxTask) { shouldRunAfter = ["deleteDistDxModule"] workingDir = file("${project.projectDir}/${subModule}") command = 'npm' args = ['install'] }); allSubModulesTasks.add ( tasks.create( 'npmBuild'+appName, NpxTask) { shouldRunAfter = ['npmInstall'+appName] workingDir = file("${project.projectDir}/${subModule}") command = 'npm' args = ['run', 'build-dxsubmodule'] }); } task buildAllSubModules(dependsOn: allSubModulesTasks){ // an array named allSubModulesTasks was dynamically generated from the subModules array to contain all needed npm calls // all tasks needed were invoked via dependsOn parameter, so now we just need to echo that it's done doLast { println 'Task buildAllSubModules done' } } // to make sure the latest rootProject.name is in the web.xml file task deleteWebXml{ delete(file("build/tmp/war/web.xml")) } task prepareWebXml (type: Copy, dependsOn: 'deleteWebXml'){ from 'src/main/config/war/web.xml' into 'build/tmp/war/' filter { it.replaceAll('@@auto-replaced-with-rootProject.name@@', rootProject.name) } } war { dependsOn buildAllSubModules, prepareWebXml webAppDirectory = file('src/main/webapp/') webXml = file('build/tmp/war/web.xml') from ('src/main/config/war/WEB-INF/') { include '*' into 'WEB-INF' filter { it.replaceAll('@@auto-replaced-with-rootProject.name@@', rootProject.name) } } from ('dist-dx-module') { include '*' into '/' } manifest { attributes 'Manifest-Version': 1 attributes 'Created-By': rootProject.name } } ear { setLibDirName(null) deploymentDescriptor { setDisplayName(rootProject.name) webModule(rootProject.name+'.war', '/'+rootProject.name) setVersion('1.4') } from ('src/main/config/ear/META-INF/') { include '*' into 'META-INF' } manifest { attributes 'Manifest-Version': 1 attributes 'Created-By': rootProject.name } } def getParamValue(String paramName){ if (project.hasProperty(paramName)){ return project.getProperty(paramName) } if (System.getProperties().containsKey(paramName)){ return System.getProperty(paramName) } return "" } def checkRequiredParam(String paramName){ if (getParamValue(paramName).trim()==""){ throw new GradleException("#### Required gradle parameter or env value: -D$paramName=<****> OR -P$paramName=<****> ") } } task deployDxModule(type:Exec, dependsOn:'ear') { doFirst{ checkRequiredParam("dxUsername") checkRequiredParam("dxPassword") logger.quiet('cd build/libs/') logger.quiet('dxclient deploy-application'+ ' --dxUsername ****'+ ' --dxPassword ****'+ ' --dxConnectUsername ****'+ ' --dxConnectPassword ****'+ ' --applicationFile '+ rootProject.name+'.ear'+ ' --applicationName '+ rootProject.name+ ' --dxProtocol '+ project.getProperty("dxProtocol")+ ' --hostname '+ project.getProperty("dxHostname")+ ' --dxPort '+ project.getProperty("dxPort")+ ' --dxProfileName '+ project.getProperty("dxProfileName")); } workingDir './build/libs' environment DXCLIENT_DISABLE_INTERACTIVE:'true' commandLine dxClientExec, 'deploy-application', '--dxUsername', getParamValue("dxUsername"), '--dxPassword', getParamValue("dxPassword"), '--dxConnectUsername', getParamValue("dxUsername"), '--dxConnectPassword', getParamValue("dxPassword"), '--applicationFile', rootProject.name+'.ear', '--applicationName', rootProject.name, '--dxProtocol', project.getProperty("dxProtocol"), '--hostname', project.getProperty("dxHostname"), '--dxPort', project.getProperty("dxPort"), '--dxProfileName', project.getProperty("dxProfileName") standardOutput = new ByteArrayOutputStream() ext.output = { return standardOutput.toString() } } def allScriptAppsTasks = [] def baseFolder = project.getProperty("baseFolder") def scriptAppFolders = project.getProperty("scriptAppFoldersCSV").split(",") scriptAppFolders.each{ folder -> def appName = folder.substring(folder.lastIndexOf("/")+1).trim().replaceAll('-','').replaceAll('_',''); def appBaseFolder = baseFolder+folder allScriptAppsTasks.add ( tasks.create( 'npmCleanStored'+appName) { delete(file(appBaseFolder+"/store/dist-dx-scriptapp/")) }); allScriptAppsTasks.add ( tasks.create( 'npmInstall'+appName, NpxTask) { shouldRunAfter = ['deployDxModule'] workingDir = file(appBaseFolder) command = 'npm' args = ['install'] }); allScriptAppsTasks.add ( tasks.create( 'npmBuild'+appName, NpxTask) { dependsOn = ['npmInstall'+appName] shouldRunAfter = ['deployDxModule'] workingDir = file(appBaseFolder) command = 'npm' args = ['run', 'build'] }); allScriptAppsTasks.add ( tasks.create( 'deploy'+appName, NpxTask) { dependsOn = ['npmBuild'+appName, 'npmCleanStored'+appName] shouldRunAfter = ['deployDxModule'] environment = ['dxUsername':getParamValue("dxUsername"), 'dxPassword':getParamValue("dxPassword"), 'dxProtocol':project.getProperty("dxProtocol"), 'dxHostname':project.getProperty("dxHostname"), 'dxPort':project.getProperty("dxPort"), 'DXCLIENT_DISABLE_INTERACTIVE':'true'] workingDir = file(appBaseFolder) command = 'npm' args = ['run', os.windows?'dx-deploy-app-use-env-win':'dx-deploy-app-use-env'] }); } task deployAllScriptApps(dependsOn: allScriptAppsTasks){ // an array named allScriptAppsTasks was dynamically generated from the scriptApps array to contain all needed npm calls // all tasks needed were invoked via dependsOn parameter, so now we just need to echo that it's done doLast { println 'Task deployAllScriptApps done' } } task deployAll(dependsOn: ['deployDxModule', 'deployAllScriptApps']){ // all tasks needed were invoked via dependsOn parameter doLast { println 'Task deployAll done' } } if (project.hasProperty("nodeInstall")) { // Workaround node grade plugin not working on apple silicon https://github.com/node-gradle/gradle-node-plugin/issues/154 Boolean downloadNode = !os.isMacOsX() || arch.isAmd64() node { version = project.getProperty("nodeVersion") npmVersion = project.getProperty("npmVersion") download = downloadNode } // Copy local node and npm to a fixed location for npmw def fixedNode = tasks.register("fixedNode", Copy) { from nodeSetup into 'build/node' } tasks.named("nodeSetup").configure { finalizedBy fixedNode } def fixedNpm = tasks.register("fixedNpm", Copy) { from npmSetup into 'build/node' } tasks.named("npmSetup").configure { finalizedBy fixedNpm } }
Add Webpack Files
- Add a webpack file to each of the submodules. Assign a unique filename(path) for the DLL Manifest that could describe the submodule.
- DxModule/SubModuleReact16/webpack.react16.js and DxModule/SubModuleReact18/webpack.react18.js
const path = require("path"); const TerserPlugin = require("terser-webpack-plugin"); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { DllPlugin } = require('webpack'); const svgToMiniDataURI = require('mini-svg-data-uri'); module.exports = { entry: { dxmodules: './modules-index.js' }, mode: "production", target: 'web', node: { global: true }, output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "dist-dx-module"), library: "[name]_[fullhash]" }, plugins: [ new DllPlugin({ name: "[name]_[fullhash]", path: path.resolve(__dirname, "./dx-dll-manifest.json"), format: true, }), new MiniCssExtractPlugin({ filename: "[name].bundle.css", }) ], resolve: { extensions: ['.ts', '.js', '.json'] }, optimization: { minimizer: [ new TerserPlugin(), ] }, module: { rules: [ { test: /\.(js|mjs|jsx|ts|tsx)$/, use: 'ts-loader', exclude: /node_modules/, }, { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ], exclude: [ /smart-/ ] }, { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader' ], }, { test: /\.svg/, type: 'asset/inline', generator: { dataUrl: content => { content = content.toString(); return svgToMiniDataURI(content); } } }, { test: /\.(png|jpg|jpeg|gif|woff|woff2|ttf|eot)$/, type: 'asset' }, ] }, };
- DxModule/SubModuleReact16/webpack.react16.js
... new DllPlugin({ name: "[name]_[fullhash]", path: path.resolve(__dirname, "./dx-dll-manifest-react16.json"), format: true, }), ...
- DxModule/SubModuleReact16/webpack.react18.js
... new DllPlugin({ name: "[name]_[fullhash]", path: path.resolve(__dirname, "./dx-dll-manifest-react18.json"), format: true, }), ...
- DxModule/SubModuleReact16/webpack.react16.js and DxModule/SubModuleReact18/webpack.react18.js
Setup WebSphere Files
-
Create the plugin.xml file in the DxModule/src/main/config/war/WEB-INF folder. Note that each generated JS and CSS files in the dist-dx-module folder after a DX Module build needs to be declared in the plugin.xml file.
- Single Sub Module
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?eclipse version="3.0"?> <plugin id="@@auto-replaced-with-rootProject.name@@" name="@@auto-replaced-with-rootProject.name@@" version="1.0.0" provider-name="IBM"> <extension point="com.ibm.portal.resourceaggregator.module" id="@@auto-replaced-with-rootProject.name@@"> <module id="@@auto-replaced-with-rootProject.name@@"> <contribution type="head"> <sub-contribution type="js"> <uri value="res:{war:context-root}/dxmodules.bundle.js"/> <uri type="debug" value="res:{war:context-root}/dxmodules.bundle.js"/> </sub-contribution> </contribution> <contribution type="head"> <sub-contribution type="css"> <uri value="res:{war:context-root}/dxmodules.bundle.css"/> <uri type="debug" value="res:{war:context-root}/dxmodules.bundle.css"/> </sub-contribution> </contribution> </module> </extension> </plugin>
- Multiple Sub Modules
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?eclipse version="3.0"?> <plugin id="@@auto-replaced-with-rootProject.name@@" name="@@auto-replaced-with-rootProject.name@@" version="1.0.0" provider-name="IBM"> <extension point="com.ibm.portal.resourceaggregator.module" id="@@auto-replaced-with-rootProject.name@@"> <module id="@@auto-replaced-with-rootProject.name@@"> <contribution type="head"> <sub-contribution type="js"> <uri value="res:{war:context-root}/react16.bundle.js"/> <uri type="debug" value="res:{war:context-root}/react16.bundle.js"/> </sub-contribution> </contribution> <contribution type="head"> <sub-contribution type="js"> <uri value="res:{war:context-root}/react18.bundle.js"/> <uri type="debug" value="res:{war:context-root}/react18.bundle.js"/> </sub-contribution> </contribution> <contribution type="head"> <sub-contribution type="css"> <uri value="res:{war:context-root}/react16.bundle.css"/> <uri type="debug" value="res:{war:context-root}/react16.bundle.css"/> </sub-contribution> </contribution> <contribution type="head"> <sub-contribution type="css"> <uri value="res:{war:context-root}/react18.bundle.css"/> <uri type="debug" value="res:{war:context-root}/react18.bundle.css"/> </sub-contribution> </contribution> </module> </extension> </plugin>
- Single Sub Module
-
Create the was.policy file in the DxModule/src/main/config/ear/META-INF folder:
// // Template policy file for enterprise application. // Extra permissions can be added if required by the enterprise application. // // NOTE: Syntax errors in the policy files will cause the enterprise application FAIL to start. // Extreme care should be taken when editing these policy files. It is advised to use // the policytool provided by the JDK for editing the policy files // (WAS_HOME/java/jre/bin/policytool). // grant codeBase "file:${application}" { permission java.security.AllPermission; }; grant codeBase "file:${jars}" { permission java.security.AllPermission; }; grant codeBase "file:${connectorComponent}" { permission java.security.AllPermission; }; grant codeBase "file:${webComponent}" { permission java.security.AllPermission; }; grant codeBase "file:${ejbComponent}" { permission java.security.AllPermission; };
-
Create the ibm-web-bnd.xmi file in the DxModule/src/main/config/war/WEB-INF folder:
<?xml version="1.0" encoding="UTF-8"?> <webappbnd:WebAppBinding xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:webappbnd="webappbnd.xmi" xmi:id="WebAppBinding_1203959801006" virtualHostName="default_host"> <webapp href="WEB-INF/web.xml#@@auto-replaced-with-rootProject.name@@"/> </webappbnd:WebAppBinding>
-
Create the ibm-web-ext.xmi file in the DxModule/src/main/config/war/WEB-INF folder:
<?xml version="1.0" encoding="UTF-8"?> <webappext:WebAppExtension xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:webappext="webappext.xmi" xmi:id="WebAppExtension_1203959801006" reloadInterval="3" reloadingEnabled="true" additionalClassPath="" fileServingEnabled="true" directoryBrowsingEnabled="false" serveServletsByClassnameEnabled="false" preCompileJSPs="false"> <webApp href="WEB-INF/web.xml#@@auto-replaced-with-rootProject.name@@"/> <jspAttributes xmi:id="JSPAttribute_1" name="jdkSourceLevel" value="15"/> <jspAttributes xmi:id="JSPAttribute_2" name="keepgenerated" value="false"/> </webappext:WebAppExtension>
-
Create the template web.xml file in the DxModule/src/main/config/war folder:
<?xml version="1.0" encoding="UTF-8"?> <web-app id="@@auto-replaced-with-rootProject.name@@" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>@@auto-replaced-with-rootProject.name@@</display-name> <context-param> <description>A regular expression that defines which of the resources in the war file can be served by the portal res datasource.</description> <param-name>com.ibm.portal.resource.whitelist</param-name> <param-value>.*</param-value> </context-param> <context-param> <description>A regular expression that defines which of the resources in the war file cannot be served by the portal res datasource.</description> <param-name>com.ibm.portal.resource.blacklist</param-name> <param-value>WEB-INF/.*</param-value> </context-param> </web-app>