Build Stack
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 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/testing';
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": "23.2.15"
}
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 Application
. 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 and TypeScript code. The configuration we use is stored in the module @eclipse-scout/eslint-config. If you like, you can use the same configuration 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": "23.2.15",
"eslint": "8.55.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.
|
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.
Authoring Libraries
If you are writing a Scout JS library module, you probably want to prebuild your JavaScript code so the consumer of your library don’t have to do it. This is especially important if you write your module in TypeScript because the Scout build does not transpile TypeScript code of libraries by default.
Basically, the following things are necessary to create a library:
-
Edit your
webpack.config.js
or create a new one if you don’t have one already. -
Create a new entry point and define a name for your library.
-
Use the
libraryConfig
function provided by Scout to create a library configuration.
The library config takes care of defining the externals (the code that won’t be bundled into your library) based on the dependencies of yourpackage.json
. This means, if animport
points to code from a dependency (or dev-, optional-, bundled- or peer-dependency), the code won’t be bundled and the import is preserved. The library itself will be an ESM module and provide anexport
so it can be imported by consumers. -
Link the entry point (
main
) of yourpackage.json
to the entry point of your library. -
Define where your entry point and your type declarations are in a bundled module using
publishConfig
. The library will be created in thedist
folder, as well as the type declarations (see theoutDir
property of yourtsconfig.json
). The attributes in thepublishConfig
will replace the regular attributes when the library is published. During development, thepublishConfig
has no effect. -
Ensure the
dist
folder will be part of the bundled module by adding it to the list of files inpackage.json
. -
In the pom.xml of the module add the following property:
<properties> <master_skip_copy_webpack_build_output>true</master_skip_copy_webpack_build_output> </properties>
This prevents the Maven build to include the transpiled JavaScript code in the jar. It is not required there as it will be provided by the Node module instead.
-
Consult the Webpack Guide if you want to learn more about authoring libraries in general.
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
args.resDirArray = [];
const config = baseConfig(env, args);
return {
entry: {
'your-library': './src/main/js/index.ts' (2)
},
...baseConfig.libraryConfig(config) (3)
};
};
{
"main": "./src/main/js/index.ts",
"publishConfig": {
"main": "./target/dist/dev/your-library.js",
"types": "./target/dist/d.ts/index.d.ts"
},
"files": [
"src/main/js",
"target/dist"
]
}
Multiple Libraries in a Single Module
If you need to build multiple libraries in a single npm module, you can use the run
argument of Scout`s webpack build to control which library should be built.
Scout itself for example uses it to build a testing library (@eclipse-scout/core/testing
) with some utilities for writing tests.
For each library specified by the run
argument a new webpack build is started, so the libraries are built consecutively which is ideal for the memory consumption.
To use this feature, just check for the run
args in your webpack config and load another config instead.
module.exports = (env, args) => {
args.resDirArray = [];
if (args.run == 'other') {
return require('./webpack.config.other.js')(env, args);
}
// ... default config
};
In the webpack.config.other.js
define your other library.
If it should only contain a part of the source code, make sure to specify the externals correctly.
The function rewriteIndexImports
of the base config may help you to externalize all imports to the index
file.
const baseConfig = require('@eclipse-scout/cli/scripts/webpack-defaults');
module.exports = (env, args) => {
const config = baseConfig(env, args);
let otherConfig = {
entry: {
'other-library': './src/main/js/folder-with-other-library/index.ts'
},
...baseConfig.libraryConfig(config)
};
otherConfig.externals = [
baseConfig.rewriteIndexImports('@your/core', 'folder-with-other-library'),
otherConfig.externals
];
return otherConfig;
};
Then, make sure every library will be built by specifying the run argument in the package.json
:
{
"scripts": {
"build:dev": "scout-scripts build:dev --run default other",
"build:prod": "scout-scripts build:prod --run default other"
}
}
Finally, you need to define the entry points in your package.json
.
You can do so using the exports
attribute.
{
"exports": {
".": "./src/main/js/index.ts",
"./other": "./src/main/js/folder-with-other-library/index.ts",
"./src/main/js/*": "./src/main/js/*"
},
"main": "./src/main/js/index.ts",
"publishConfig": {
"exports": {
".": "./target/dist/dev/your-library.js",
"./testing": "./target/dist/dev/other-library.js",
"./src/main/js/*": "./src/main/js/*"
},
"main": "./target/dist/dev/your-library.js"
}
}
Important: As soon as the exports
attribute is used, consumers of the module can only import code that is explicitly exported.
In order to still allow imports to individual source files directly (e.g. for CSS Files), you can export them using wildcard syntax (./src/main/js/*
).
If you don’t need or want that, just omit this configuration.