Build Stack
This document is referring to a past Scout release. Please click here for the recent version. |
JavaScript and CSS assets of a typical Scout application are built by Webpack using npm and Node.js. In order to make the building as easy as possible for you, there is a CLI module available. That module contains a default webpack and karma configuration and several build scripts you can use. The goal is to reduce the time you need to setup your build to a minimum. If you have created your Scout project using a Scout archetype, it should all be already setup for you. Nevertheless, you will get to a point where it is important to know how the building works in detail and how the several build tools are wired together. If you are there, this chapter should help you out.
Dependency Management
In every modern application you will have dependencies to other modules, either modules you created to separate your code, or third party modules like Scout. Such dependencies to other JavaScript modules are managed by the Node Package Manager (npm). So every module containing JavaScript or Less code needs to be a Node module with a package.json
file that defines its dependencies.
This setup gives you the possibility to easier integrate and update 3rd party JavaScript frameworks available in the huge npm registry.
Scout itself is also published to that registry and will therefore be downloaded automatically once you execute npm install
, as long as your package.json
contains a Scout dependency. You will recognize a Scout module based on its name: all official Scout modules are published using the scope @eclipse-scout
. The most important one is @eclipse-scout/core which contains the core runtime functionality. Other modules are @eclipse-scout/cli for the building support, @eclipse-scout/eslint-config for our ESLint rules, or @eclipse-scout/karma-jasmine-scout for enhanced testing support.
ES6 Modules
In addition to Node module dependencies, a Scout application uses ES6 imports to define dependencies between each JavaScript files. So if you want to use a class or utility from @eclipse-scout/core
, you’ll need to import that class or utility in your own JavaScript file.
import PersonFormModel from './PersonFormModel';
import {Form, models} from '@eclipse-scout/core';
export default class PersonForm extends Form {
_jsonModel() {
return models.get(PersonFormModel);
}
}
In the code above there are two imports defined: the first one imports the file PersonFormModel
into the variable PersonFormModel. The second one imports the class Form
and the utility models
from the scout core module. Notice that the first import directly addresses a specific file while the second import addresses the node module itself. This is possible because Scout provides an index
file specifying all available exports. That file is linked in the package.json
. If your application contains more than one Node modules as well, you can do the same.
Webpack Configuration
Scout provides a default Webpack configuration containing all the necessary settings for Webpack and the plugins needed for a typical Scout application setup. To make your application use the Scout defaults, you need to create a file called webpack.config.js
in your Node module and reexport the Scout configuration.
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
return baseConfig(env, args);
};
If you don’t like the defaults you can easily adjust them by customizing the object returned by the baseConfig(env, args)
call.
Beside using the default configuration, you’ll need to configure some small things in order to make your application work. In this chapter we’ll have a look at these things you have to configure and the things that are provided by default.
Bundling
The main purpose of Webpack is to bundle the many small source files into one or a few larger JavaScript or CSS files which are included in the HTML files as <script>
resp. <style>
tags and therefore loaded by the browser.
Scout does not provide any special bundling rules, but relies on the Webpack default configuration. It is optimized for best performance and user experience on modern browsers. If you want to customize the bundling please have a look at the SplitChunksPlugin of Webpack.
To let Webpack know about your entry files you need to specify them in your webpack.config.js
.
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
const config = baseConfig(env, args);
config.entry = {
'helloworld': './src/main/js/index.js',
'helloworld-theme': './src/main/js/theme.less',
'helloworld-theme-dark': './src/main/js/theme-dark.less'
};
return config;
};
In this example the application is called helloworld
and there is a bundle created with the same name.
In order to create the bundle, Webpack uses the entry file, which is index.js
in this case, follows all the ES 6 imports and includes these files.
It then extracts chunks into separate files based on the predefined Webpack default rules.
So you don’t have to care about these chunks unless you want to customize it.
Also notice that the same applies to CSS files. The above example defines 2 CSS bundles in addition to the JavaScript bundle: helloworld-theme.css
and helloworld-theme-dark.css
. There are no predefined chunks for CSS files, we just put all the CSS code in one big file.
Static Web Resources
In addition to JavaScript and CSS resources bundled by webpack, your application will probably also require resources like images or fonts. Such resources should be placed in a resource folder, e.g. src/main/resources/WebContent
if you use the Maven module structure, or just res
otherwise. Because there are multiple modules that could provide such resources, you need to specify them in your webpack.config.js
using the resDir
array.
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
args.resDirArray = ['src/main/resources/WebContent', 'node_modules/@eclipse-scout/core/res'];
return baseConfig(env, args);
};
In the snippet above the resDir
array contains a folder of your module and a folder of Scout itself. The resource folder of Scout mainly contains the scoutIcons.woff
, which is the icon font used by some Scout widgets.
When the build runs all the folders specified by the resDir
array are visited and the resources collected. These resources are then available under /
(if you use the Scout backend). If you want to know how to start the build, have a look at the Command Line Interface (CLI).
EcmaScript Transpiler
In order to use the latest EcmaScript features like the ES6 Modules but still support older browsers, Scout uses Babel to transpile ES6+ code into ES5. The transpiler is enabled by default if you use the Webpack configuration provided by Scout, so you don’t have to configure it by yourself.
CSS Preprocessor
The CSS preprocessor used by Scout is Less, so the default webpack configuration already supports it by using the less-loader
plugin. In order to profit from Scout`s less variables (see Styling]) we recommend to use Less as well. Since it is already configured, you won’t have to do anything but to write your CSS rules.
Karma Configuration
Scout uses Karma as test runner for its unit tests. The tests itself are written with the test framework Jasmine. We also use some plugins like karma-jasmine-jquery, karma-jasmine-ajax or karma-jasmine-scout to make writing tests for a Scout application even easier.
All this is configured in the file karma-defaults.js
. If you want to use them too, you need to provide your own Karma file called karma.conf.js
and import the defaults, similar to the Webpack Configuration. You can now adjust or override the defaults or just leave them as they are. To let Karma know about your tests, you need to define the entry point.
const baseConfig = require('@eclipse-scout/cli/scripts/karma-defaults');
module.exports = config => baseConfig(config, './src/test/js/test-index.js');
In the snippet above you see two things: The Scout defaults are imported and the entry point test-index.js
is defined. This is all you need to do in this file if you are fine with the defaults.
The file test-index.js
defines where your unit tests are and what the context is for the Webpack build. Because a unit test is called a spec
when using Jasmine
, a typical test-index.js
looks like this:
import {JasmineScout} from '@eclipse-scout/core/src/testing/index';
let context = require.context('./', true, /[sS]pec\.js$/);
JasmineScout.runTestSuite(context);
This code tells the karma-webpack plugin to require all files ending in Spec.js
.This will generate one big test bundle, but since source maps are enabled, you can debug the actual test files easily.The last line installs the given context and also runs a Scout app so that the Scout environment is properly set up.
Reporting
After running the tests, all results are put in a folder called test-results
.There is a sub folder for each browser that executed the tests containing a file called test-results.xml
.Since the karma-defaults.js
uses the junit
reporter, the file can be interpreted by any tool supporting the junit
format, e.g. Jenkins.
Command Line Interface (CLI)
The Scout CLI is a bunch of npm-scripts that help you building and testing your application.In order to use them you need to add a devDependency to @eclipse-scout/cli to the package.json
of your module.We also suggest to add some scripts to make the execution easier.If you use the Scout archetype, the following will be created for you.
"scripts": {
"testserver:start": "scout-scripts test-server:start",
"testserver:stop": "scout-scripts test-server:stop",
"test:ci": "scout-scripts test:ci",
"build:dev": "scout-scripts build:dev",
"build:prod": "scout-scripts build:prod",
"build:all": "scout-scripts build:dev && scout-scripts build:prod",
"build:dev:watch": "scout-scripts build:dev:watch"
},
"devDependencies": {
"@eclipse-scout/cli": "10.0.0"
}
Building
Before you can open your application in the browser, you need to build it. The build takes all your source code and resources and creates the artifacts needed for the browser according to your Webpack Configuration. Once the build is complete all the produced artifacts are put in the target/dist
folder.
The target/dist
folder contains three sub folders:
-
dev: contains not minified versions of the JS and CSS bundles with Source Maps. The source maps are necessary to map the bundles to the actual source files which makes debugging a lot easier. The Scout server delivers such bundles if it runs in dev mode (
scout.devMode=true
). -
prod: contains minified versions of the JS and CSS bundles with restricted source maps (the maps don’t contain the actual source code, only the information necessary to create meaningful stack traces, see also the devtool property
nosources-source-map
). Content hashes are generated and added to the bundles for optimal cashing. The Scout server delivers such bundles if it runs in production mode (scout.devMode=false
). -
res: contains all static resources from the various resource folders specified by the
resDir
array, see Static Web Resources.
If the property scout.urlHints.enabled is set to true , the dev files can be requested on the fly even if the server does not run in devMode . Just add the query parameter ?debug=true and the files in the dev folder instead of the ones in the prod folder are delivered. This can be very useful to debug a deployed application.
|
In order to start the build, use the following command:
npm run build:dev
This will fill the dev and res folders with the appropriate files. To make the files available to your browser you need to start a webserver. When using the Scout backend just start the class JettyServer
. Once the build is complete and Jetty runs, you can open your application in the browser.
If you now make adjustments on your JS or CSS files, you would have to rerun the buid script, which could be time consuming and annoying. To make your developer life easier you can run the following script instead:
npm run build:dev:watch
This will also build your application but additionally starts a watcher that watches your source code. As soon as you change your code that watcher will notice and start a build. Since it knows which files changed, only these files need to be rebuilt which makes it a lot faster.
Arguments
The build commands accept some arguments you can use to adjust the build without modifying your webpack config file. The following arguments are available:
-
mode:
development
orproduction
. This argument is set automatically when using build:dev or build:prod. -
clean: true, to clean the
target/dist
folder before each build. Default isfalse
if watcher is enabled (build:dev:watch), otherwisetrue
. -
progress:
true
, to show build progress in percentage. Default istrue
. -
profile:
true
, to show timing information for each build step. Default isfalse
. -
resDirArray: an array containing directories which should be copied to
dist/res
. -
stats: object to control the build output. There are some presets available as shortcuts (e.g. 'detailed' or 'errors-only'), see also: https://webpack.js.org/configuration/stats/.
In order to set an argument make sure to separate the arguments using -- from the command. Example:
npm run build:dev -- --progress false
All arguments are passed to the webpack config file as parameter args
which is the second parameter. The first parameter called env
is actually just a convenience accessor to args.env
and does not contain system environment variables. If you want to access them just use the regular node syntax process.env
.
Testing
Before you can run your unit tests you need to properly setup the files as described in Karma Configuration.
If all is setup correctly, you can run your tests using the following command:
npm run test:ci
This will execute all unit tests with the headless browser. The default headless browser is Chrome, so you need to make sure Chrome is installed. This includes your Continuous Integration Environment, if you plan to automatically run the tests on a regular basis (e.g. with Jenkins).
The above command will execute the tests once and does not watch for changes. This is typically not desired during development. When you are actively developing a component and want to run your tests while you are developing, you can use the following command:
npm run testserver:start
This will start a real browser and enable the watch mode. This means every time you adjust your code and save it, the web pack build is started, the browser reloaded and your tests executed.
If you don’t like the automatic browser reloading, you can press debug on the top right corner of the browser or manually navigate to http://localhost:9876/debug.html. |
Arguments
The test commands accept some arguments you can use to adjust the karma runner without modifying your karma config file. All passed arguments are merged with the karma config object, so all karma configuration options are available (see http://karma-runner.github.io/4.0/config/configuration-file.html).
Example usage:
npm run test:ci -- --junitReporter.outputDir=custom-out-dir
Please note that no type conversion happens which is especially relevant for boolean arguments. If you for example want to disable the watcher, you cannot use --auto-watch false . Instead, you would have to use --no-auto-watch .
|
In addition to the karma configuration options you can also pass the webpack arguments (checkout Arguments for a list of available arguments). To do that, you need to use the argument called webpackArgs
. Example:
npm run testserver:start -- --webpackArgs.progress=false
test:ci automatically disables the webpack progress because you don’t want the progress when the tests run on a continuous integration server. |
Test prod scripts on your local machine
In case you need to test the files built by build:prod
locally, follow this procedure:
-
Stop the UI server.
-
Run
npm run build:prod
, this script will copy minified script files to the /dist folder. -
Start the UI server. Stopping and starting the UI server makes sure the server-side script cache is cleared.
-
Start the application with the URL parameter
/?debug=false
. -
Check your index.html in the browser. Each referenced script or CSS file should have a fingerprint, example:
yourapp-2c6053b2fdf5b816fae5.min.js
.
If you set the config property scout.devMode to false instead of using the URL parameter, the resources will be loaded from the Java classpath.
In that case you need to additionally copy the content of the dist folder to target/classes before starting the UI server.
Or you can also set scout.loadWebResourcesFromFilesystem to true to disable classpath loading (see also LoadWebResourcesFromFilesystemConfigProperty ).
|
ESLint
For the Scout code base we use ESLint to analyze the JavaScript code. The ruleset we use is stored in the module @eclipse-scout/eslint-config. If you like, you can use the same ruleset for your application, but you don’t have to. You can use your custom config or even a different linter.
When using the Scout archetype to generate your app, the ESLint configuration is already setup for you and you don’t need to do the following steps. |
In order to use the Scout eslint-config, you need to add devDependencies to the modules @eclipse-scout/eslint-config
and eslint
in your package.json
.
"devDependencies": {
"@eclipse-scout/eslint-config": "22.0.0",
"eslint": "8.10.0"
}
Then create a file called .eslintrc.js
with the following content:
module.exports = {
extends: '@eclipse-scout'
};
This tells ESLint to inherit the configuration from the Scout module. In order to run the analysis, you can either use an IDE that supports it (e.g. IntelliJ), or the command line.
npx eslint .
If the command takes very long and prints a lot of errors, you may have to ignore the target/dist folder, see ESLint Ignore.
|
The command above will analyze your current directory including all sub directories. Depending on your environment, it is likely that you’ll see some errors regarding linebreaks. This is because the Scout config enforces the UNIX format (LF). You can now either convert the linebreaks of your files to that format and adjust your editor to always use the UNIX format, or you can disable the rule. To do that, just add the following to your .eslintrc.js
:
rules: {
'linebreak-style': 'off'
}
Now run the command again to make the linebreak errors disappear.
If you plan to configure your IDE to use the UNIX linebreak format, we recommend having a look at Editor Config. The file can be interpreted by various IDEs. Just add end_of_line=lf to that file and you are done.
|
Babel Dependency
If you use some bleeding edge EcmaScript features that are not yet part of the official specification but already supported by Babel, you should add a dependency to the babel-eslint plugin. Otherwise the analysis will probably report an error regarding these features.
One example of such a feature is class properties
. This allows the definition of static class members. Scout itself uses that feature, that is why the Scout CLI has a dependency to babel-plugin-proposal-class-properties.
class Example {
static anObject = {};
}
If you plan to use such features too, you should enable the babel eslint parser. To do that, add the following devDependencies to your package.json
:
"devDependencies": {
"@babel/eslint-parser": "7.16.5",
"@babel/eslint-plugin": "7.16.5"
}
To enable it, configure your .eslintrc.js
in the following way:
plugins: ['@babel'],
parser: '@babel/eslint-parser',
parserOptions: {
requireConfigFile: false
}
That’s it.
Remember: ESLint itself already supports a lot of modern EcmaScript code. You only need to enable the babel eslint parser if you want to use the latest features which are not yet supported by ESLint.
ESLint Ignore
Similar to .gitignore
, you can create a file called .eslintignore
to exclude specific files or directories from the analysis.
Because analyzing the build output probably does not make any sense, we recommend to at least ignore the target
folder.
The only thing you need to do is to create that file and add a line with the word target
.
For more details please see the official ESLint documentation at https://eslint.org/docs/user-guide/configuring#eslintignore.