Quarkus 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 Template Engine 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).
|
Why is the directory different from other Qute app (i.e. src/main/resources/templates/
)?
When used in src/main/resources/web
the rendering is happening at build time (and doesn’t require Qute at runtime). It only offers partial support, 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 with Quarkus Qute.
Web Live-Coding
The Web Bundler enable browser live coding for development. This is done through a small injected script (only in dev-mode). This script will listen for changes from the server.
In this mode, script and styles will be watched by esbuild and won’t require any restart from Quarkus.
The page will be:
-
hot-reloaded (no refresh) when a style is modified (css, scss).
-
refreshed when a js, template or java file is modified.
When web live coding is enabled, to allow it to work properly, minification and bundle hashes are disabled in dev-mode.
templates and java change can take a bit longer (1s) to be detected as there is no file-system watch implemented in Quarkus yet. |
This can be disabled through browser-live-reload config.
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. |
import $ from 'jquery';
import 'bootstrap/dist/css/bootstrap.css';
$('.hello').innerText('Hello');
Styles can be also be imported from a scss file:
@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:
import './my-script.js'
import './my-style.scss'
import './example.png'
Entry-Points
You may configure different entry-points (to generate different bundles):
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):
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:
...
<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). |
Browser live-reload
Browser live reload is enabled by default:
-
auto-inject a live-reload script in the bundle to watch for changes through sse (server-sent event)
-
supersonic in-page replace for css/scss changes
-
supersonic page refresh for javascript changes
-
watch other Quarkus files such as html, java, … (unless there is an error in which case a new request is needed)
This is enabled by default. To disable:
quarkus.web-bundler.browser-live-reload=false
When browser-live-reload is enabled:
-
bundle files will be fixed (no hashes, i.e.
main.js
) -
minification is disabled
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:
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.
quarkus.web-bundler.dependencies.type=webjars
<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. This option is enabled by default in production.
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>