Quarkus logo Web Bundler - Advanced Guides

Web Root

The Web Root is src/main/resources/web, this is where the Web Bundler will look for stuff to bundle and serve.

Static files

There are 2 ways to add static files (fonts, images, music, video, …​) to your app: - Files in src/main/resources/web/static/** will be served statically under http://localhost:8080/static/ (you can choose another directory name Config Reference). For convenience, those static files are excluded (marked as external) from the bundling by default. This allows to reference them from scripts or styles without errors (e.g. import '/static/foo.png';). - Other files imported from scripts or styles will be bundled and processed by the configured loaders (see How is it bundled (Loaders)) allowing different options (like embedding them as data-url).

html templates rendering

The Web-Bundler is natively integrated with Qute to render html templates at build-time (this won’t affect runtime). This way you may create SPA out of the box. For this, just provide an index.html file (or any other .html file) in the src/main/resources/web directory.

Combined with the {#bundle /} tag, this file will be rendered with the scripts and styles tags to include in your html template. This file will be served as the default index file (e.g. http://localhost:8080/).

You can use the Qute config: namespace, it will be evaluated a build-time (runtime config will be ignored).
This rendering option is happening at build time, so you won’t be able to access any Qute runtime data in this file. If you want to render data (for example in a server-side rendering app with htmx), you should add the Qute Web extension (or Renarde extension for MVC with Qute) and use src/main/resources/templates/ directory instead. The {#bundle /} tag will also work in this case.

Bundling

By default, directory src/main/resources/web/app is destined to contain the scripts, styles and possibly assets for your app. It will be bundled and served into /static/bundle/main-[hash].[ext]. (see Entry-Points for more options).

Importing Web Dependencies

Once added in the pom.xml, the web dependencies can be imported and used with the ESM import syntax, they will automatically be bundled.

Web Dependencies (script and styles) need to be imported in order to be bundled, dead code will be eliminated during the build.
web/app/script.js
import $ from 'jquery';
import 'bootstrap/dist/css/bootstrap.css';

$('.hello').innerText('Hello');

Styles can be also be imported from a scss file:

web/app/style.scss
@import "bootstrap/dist/scss/bootstrap.scss";

What is bundled

Indexing

Only what’s imported will be part of the resulting bundle, to make it easy, the Web Bundler will automatically generate an index importing all the files found in an entry-point directory.

Of course, you can also provide this index manually (named index.js,ts,jsx,tsx) and choose what to import. Example:

src/main/resources/web/app/index.js
import './my-script.js'
import './my-style.scss'
import './example.png'

Entry-Points

You may configure different entry-points (to generate different bundles):

src/main/resources/application.properties
quarkus.web-bundler.bundle.page-1=true (1)
quarkus.web-bundler.bundle.page-2=true (2)
1 Bundle src/main/resources/web/page-1/…​ into /static/bundle/page-1-[hash].[ext]
2 Bundle src/main/resources/web/page-2/…​ into /static/bundle/page-2-[hash].[ext]

or customize the directory name and bundled file name (and possibly merge multiple directories into one bundle):

src/main/resources/application.properties
quarkus.web-bundler.bundle.foo=true (1)
quarkus.web-bundler.bundle.bar=true (2)
quarkus.web-bundler.bundle.bar.key=my-key
quarkus.web-bundler.bundle.bar.dir=my-dir
quarkus.web-bundler.bundle.baz=true (1)
quarkus.web-bundler.bundle.baz.key=foo
1 Bundle src/main/resources/web/foo/…​ and src/main/resources/web/baz/…​ together into /static/bundle/foo-[hash].[ext]
2 Bundle src/main/resources/web/my-dir/…​ into /static/bundle/my-key-[hash].[ext]
By default, as soon as more than one entry-point key is configured, shared code and web dependencies are split off into a separate file. That way if the user first browses to one page and then to another page, they don’t have to download all the JavaScript for the second page from scratch if the shared part has already been downloaded and cached by their browser. The path of the shared static script is /static/bundle/chunk-[hash].js. This setup is perfect if you create an app with different pages using different scripts, libraries and styles.

How is it bundled (Loaders)

Bases on the files extensions, the Web Bundler will use pre-configured loaders to bundle them. For scripts and styles, the default configuration should be enough.

For other assets (svg, gif, png, jpg, ttf, …​) imported from your scripts and styles using their relative path, you may choose the loader based on the file extension allowing different options (e.g. serving, embedding the file as data-url, binary, base64, …​). By default, they will automatically be copied and served using the file loader.

For example, url('./example.png') in a style or import example from './example.png'; in a script will be processed, the file will be copied with a static name and the path will be replaced by the new file static path (e.g. /static/bundle/assets/example-QH383.png). The example variable will contain the public path to this file to be used in a component img src for example.

For convenience, when using a file located in the static directory (e.g. url('/static/example.png'), the path will not be processed because all files under /static/** are marked as external (to be ignored from the bundling). Since /static/example.png will be served by Quarkus (See Static files), it is ok.

SCSS, SASS

You can use scss or sass files out of the box. Local import are supported. Importing partials is also supported begin with _ (as in _code.scss imported with @import 'code';).

Server-Side Qute Components

This features requires quarkus-qute or quarkus-qute-web in the project (and this is not made to be used with the build-time template rendering).

This is not always needed but if you need to add specific script and/or style to your Qute tags (Server Side Qute Components). This will help you do it elegantly.

To enable server-side components, add this in the application.properties:

quarkus.web-bundler.bundle.components=true
quarkus.web-bundler.bundle.components.key=main (1)
quarkus.web-bundler.bundle.components.qute-tags=true (2)
1 use main to have a single merged bundle with the app (or remove this line to use qute-components as default)
2 activate qute-tags support (default is false)

Here is a nice convention to define your components: src/main/resources/web/components/[name]/[name].{html,css,scss,js,ts,…​};. The scripts, styles and assets will be bundled, the html template will be usable as a Qute tag.

Example: - src/main/resources/web/components/hello/hello.html - src/main/resources/web/components/hello/hello.js - src/main/resources/web/components/hello/hello.scss

This way you can use {#hello} in your templates and the scripts & styles will be bundled.

You may create different qute components groups to be used in different pages.

Web Dependencies

The Web Bundler is integrated with NPM dependencies through MVNPM (default) (default) or WebJars. Once added in the pom.xml the dependencies are directly available through import from the scripts and styles.

Using the Web Bundler, Web Dependencies are bundled, there is not point for the jars to be packaged in the resulting app. Web Dependencies with provided scope (or compileOnly with Gradle) will not be packaged in the resulting app.

INFO: By default, the Web Bundler will fail at build time if it detects non compile only Web Dependencies. You can configure a flag to allow them but keep in mind that they will be served by Quarkus.

If you don’t import a Web Dependency from an entry-point (Indexing), it won’t be bundled (dead code elimination).

MVNPM (default)

mvnpm (Maven NPM) is a maven repository facade on top of the NPM Registry.

Lookup for packages on https://mvnpm.org or https://www.npmjs.com/ then add them as web dependencies to your pom.xml:

pom.xml
...
<dependencies>
    ...
    <dependency>
        <groupId>org.mvnpm</groupId> (1)
        <artifactId>jquery</artifactId> (2)
        <version>3.7.0</version> (3)
        <scope>provided</scope> (4)
    </dependency>
</dependencies>
...
1 use org.mvnpm or org.mvnpm.at.something for @something/dep
2 All dependencies published on NPM are available
3 Any published NPM version for your dependency
4 Use provided scope to avoid having the dependency packaged in the target application

If a package or a version in not yet available in Maven Central:

  • You may use the mvnpm.org website to synchronize new versions with Maven Central (Click on the Maven Central icon)

  • If configured with the mvnpm repository, when requesting a dependency, it will inspect the registry to see if it exists and if it does, convert it to a Maven dependency and publish it to Maven Central so that future developers (and CI) won’t need the repository.

Configure the mvnpm-repo profile in your ~/.m2/settings.xml: .settings.xml

<settings>
    <profiles>
        <profile>
            <id>mvnpm-repo</id>
            <repositories>
                <repository>
                    <id>central</id>
                    <name>central</name>
                    <url>https://repo.maven.apache.org/maven2</url>
                </repository>
                <repository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>mvnpm.org</id>
                    <name>mvnpm</name>
                    <url>https://repo.mvnpm.org/maven2</url>
                </repository>
            </repositories>
        </profile>
    </profiles>
</settings>
I only use -Pmvnpm-repo when locking my project or updating mvnpm versions.
In case of web dependencies versions conflicts, you can set the version to use for this dependency in the project dependencyManagement (even when using the locker).

Node Modules for bundling

To deal with Web Dependencies, the Web Bundler manages a node_modules directory. The default location for it is the build direcotry (i.e. target/node_modules on Maven). For a good IDE support (detection of imports and types), you may use the project directory instead quarkus.web-bundler.dependencies.node-modules=node_modules and add the node_modules/ to the .gitignore, the IDE will look recursivelly in parent directories for imports and the Web Bundler too.

Locking dependencies

As the NPM ecosystem is over-using version ranges for dependencies, just a few npm dependencies can lead to a big amount of poms to download in order to get the right versions for your project. This leads to long lasting minutes of downloading poms when cloning or during CI. To solve this problem and also to make your build reproducible it is highly recommended to lock your web dependencies versions. This way it will be long only the first time and then it will be fast and reproducible.

Locking with Maven (mvnpm Locker Maven Plugin)

As Maven doesn’t provide a native version locking system, the mvnpm team has implemented a way to easily generate and use a locking pom.xml (BOM). The locker Maven Plugin will create a version locker BOM for your org.mvnpm and org.webjars dependencies. It is essential as NPM dependencies are over using ranges. After the locking, the quantity of files to download is considerably reduced (better for reproducibility, contributors and CI).

It is easy to set up and update, here is the documentation.

Locking with Gradle

Gradle provides a native version locking system, to install it, add this:

build.gradle
dependencyLocking {
    lockAllConfigurations()
}

Then run gradle dependencies --write-locks to generate the lockfile.

WebJars

Adding new dependencies or recent versions has to be done manually from their website.

WebJars are client-side web libraries (e.g. jQuery & Bootstrap) packaged into JAR (Java Archive) files. You can browse the repository from the website.

application.properties
quarkus.web-bundler.dependencies.type=webjars
pom.xml
<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>jquery</artifactId>
    <version>3.7.0</version>
    <scope>provided</scope>
</dependency>

Bundle Paths

After the bundling is done, the bundle files will be served by Quarkus under {quarkus.http.root-path}/static/bundle/…​ by default (Config Reference).

This may also be configured with an external URL (e.g. 'https://my.cdn.org/'), in which case, Bundle files will NOT be served by Quarkus and all resolved paths in the bundle and mapping will automatically point to this url (a CDN for example).

In production, it is a good practise to have a hash inserted in the scripts and styles file names (E.g.: main-XKHKUJNQ.js) to differentiate builds (make them static). This way they can be cached without a risk of missing the most recent builds.

In the Web Bundler, we also do it in dev mode, this way the app is as close as possible to production. You won’t have surprise when deploying a new version of your application.

To make it easy there are several ways to resolve the bundle files public paths from the templates and the code.

{#bundle /} tag

From any Qute template you can use the {#bundle /} tag to help insert the bundled scripts and styles in your html page. examples:

{#bundle /}
Output:
<script type="text/javascript" src="/static/bundle/main-[hash].js"></script>
<link rel="stylesheet" media="screen" href="/static/bundle/main-[hash].css">

{#bundle key="components"/}
Output:
<script type="text/javascript" src="/static/bundle/components-[hash].js"></script>
<link rel="stylesheet" media="screen" href="/static/bundle/components-[hash].css">

{#bundle tag="script"/}
Output:
<script type="text/javascript" src="/static/bundle/main-[hash].js"></script>

{#bundle tag="style"/}
Output:
<link rel="stylesheet" media="screen" href="/static/bundle/main-[hash].css">

{#bundle key="components" tag="script"/}
Output:
<script type="text/javascript" src="/static/bundle/components-[hash].js"></script>

Inject Bundle bean

This bean can be injected in the code:

@Inject
Bundle bundle;

...

System.out.println(bundle.script("main"));
System.out.println(bundle.style("main"));

or in a Qute template:

{inject:bundle.script("main")}
{inject:bundle.style("main")}