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.

Listing 1. Importing ES6 modules
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.

Listing 2. Using Scout’s default Webpack config
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.

Listing 3. Using Scout’s default Webpack config
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.

Listing 4. Specifying res folders
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.

Listing 5. karma.conf.js
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:

Listing 6. karma.conf.js
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.

Listing 7. CLI dependency and scripts in package.json
"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:

  1. 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).

  2. 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).

  3. 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:

  1. mode: development or production. This argument is set automatically when using build:dev or build:prod.

  2. clean: true, to clean the target/dist folder before each build. Default is false if watcher is enabled (build:dev:watch), otherwise true.

  3. progress: true, to show build progress in percentage. Default is true.

  4. profile: true, to show timing information for each build step. Default is false.

  5. resDirArray: an array containing directories which should be copied to dist/res.

  6. 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.

Listing 8. ESLint Dependencies
"devDependencies": {
  "@eclipse-scout/eslint-config": "23.1.0",
  "eslint": "8.27.0"
}

Then create a file called .eslintrc.js with the following content:

Listing 9. .eslintrc.js
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:

Listing 10. Disabling the linebreak rule
  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:

  1. Edit your webpack.config.js or create a new one if you don’t have one already.

  2. Create a new entry point and define a name for your library.

  3. 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 your package.json. This means, if an import 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 an export so it can be imported by consumers.

  4. Link the entry point (main) of your package.json to the entry point of your library.

  5. Define where your entry point and your type declarations are in a bundled module using publishConfig. The library will be created in the dist folder, as well as the type declarations (see the outDir property of your tsconfig.json). The attributes in the publishConfig will replace the regular attributes when the library is published. During development, the publishConfig has no effect.

  6. Ensure the dist folder will be part of the bundled module by adding it to the list of files in package.json.

  7. In the pom.xml of the module add the following property:

    <master_skip_copy_webpack_build_output>true</master_skip_copy_webpack_build_output>

    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.

  8. Consult the Webpack Guide if you want to learn more about authoring libraries in general.

Listing 11. Library Webpack Config
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)
  };
};
Listing 12. package.json for a library
{
  "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.

Listing 13. package.json for a library
{
  "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.