Quarkus - Quinoa

Quinoa is a Quarkus extension which eases the development, the build and serving single page apps or web components (built with NodeJS: React, Angular, Vue, Lit, …) alongside other Quarkus services (REST, GraphQL, Security, Events, …​).

Live code the backend and frontend together with close to no configuration. When enabled in development mode, Quinoa will start the UI live coding server provided by the target framework and forward relevant requests to it. In production mode, Quinoa will run the build and process the generated files to serve them at runtime.

How it works

The Quinoa build (using npm)

Quinoa Build
packages are installed by Quinoa before the build when needed (i.e npm install). See Packages installation. Quinoa is pre-configured to work with your favorite package manager (npm, yarn or pnpm).

Runtime for production mode

When running jar or binary in production mode:

Quinoa Runtime Production

Runtime for full Quarkus live-coding

Quinoa (using Quarkus live-coding watch feature) will watch the Web UI directory and trigger a new build on changes. It works the same as the production mode. This option is perfect for small/fast builds.

You can differentiate the build for dev mode. e.g to disable minification.

Runtime for proxied live-coding

When running dev-mode (e.g with npm on port 3000):

Quinoa Proxy Dev
Quarkus live-coding will keep watching for the backend changes as usual.

Prerequisite

  • Create or use an existing Quarkus application

  • Add the Quinoa extension

  • Install NodeJS (https://nodejs.org/) or make sure Quinoa is configured to install it.

Installation

Create a new Quinoa project (with a base Quinoa starter code):

quarkus create app quinoa-app -x=io.quarkiverse.quinoa:quarkus-quinoa

Then start the live-coding:

quarkus dev

You could also just add the extension (but you won’t get the starter code):

quarkus ext add io.quarkiverse.quinoa:quarkus-quinoa
  • In your pom.xml file, add this dependency:

<dependency>
    <groupId>io.quarkiverse.quinoa</groupId>
    <artifactId>quarkus-quinoa</artifactId>
    <version>2.0.2</version>
</dependency>

Getting Started

If not yet created by the tooling, you will need a Web UI directory in src/main/webui. This directory will contain your NodeJS Web application code with a package.json. The location is configurable, the directory could be outside the Quarkus project as long as the files are available at build time.

quarkus.quinoa.ui-dir=../my-webui

From here, copy your existing Web UI or generate an application from any existing Node based Web UI framework such as React, Angular, Lit, Webpack, Rollup, …​ or your own. Example:

The key points are the package.json scripts (build and optionally test) and the directory where the web files (index.html, scripts, …​) are generated (by default, it will use build/ relative to the ui-dir).

Quinoa provides two options for live-coding:

  • Delegate to the UI live-coding dev server (proxy mode). To enable it, configure the port of the UI server. By convention Quinoa will call the start script from the package.json to start the UI server process. Then it will transparently proxy relevant requests to the given port.

  • Quarkus watches the files and Quinoa triggers a new Web UI build on changes (you can configure different builds for dev and prod).

Start the Quarkus live coding:

$ quarkus dev

It’s done! The web application is now built alongside Quarkus, dev-mode is available, and the generated files will be automatically copied to the right place and be served by Quinoa if you hit http://localhost:8080

2022-03-28 09:24:46,739 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa target directory: 'xxx/target/quinoa-build'
2022-03-28 09:24:46,739 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/favicon.ico'
2022-03-28 09:24:46,740 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/index.html'
2022-03-28 09:24:46,741 INFO  [io.qua.qui.dep.QuinoaProcessor] (build-25) Quinoa generated resource: '/simple-greeting.js'
With Quinoa, you don’t need to manually copy the files to META-INF/resources. Quinoa has its own system and will provide another Vert.x route for it. If you have conflicting files with META-INF/resources, Quinoa will have priority over them.

How to use the extension.

Configure the build

Add a build script in the package.json to generate your web application index.html, scripts and assets (styles, images, …​) in some build directory (configurable [quarkus-quinoa_quarkus.quinoa.build-dir].

 "scripts": {
    "start": "[start the Web UI live coding server]",
    "build": "[build the Web UI]",
    "test": "[test the Web UI]"
  },
The build directory will automatically be moved by Quinoa to target/quinoa-build when using Maven (build/quinoa-build with Gradle) in order to be served.

You can differentiate development from production builds using the environment variable NODE_ENV (production/development). by-node-env can help you if you have different build commands:

"scripts": {
  "build": "by-node-env",
  "build:development": "...",
  "build:production": "...",
},
"devDependencies": {
  "by-node-env": "~2.0.1"
}

Package manager

Quinoa can be configured to install NodeJS and NPM in the project directory:

quarkus.quinoa.package-manager-install=true (1)
quarkus.quinoa.package-manager-install.node-version=16.17.0 (2)
1 Enable package manager install
2 Define the version of NodeJS to install
By default, NodeJS and NPM will be installed in {project-dir}/.quinoa/ (can be configured). If not specified, it will use the NPM version provided by NodeJS.

If NodeJS and NPM are not installed by Quinoa, it is possible to override the package manager (NPM, Yarn or PNPM), otherwise, it will be auto-detected depending on the project lockfile (NPM is the fallback):

  • Use quarkus.quinoa.package-manager if present

  • Else if yarn.lock then Yarn

  • Else if pnpm-lock.yaml then PNPM

  • Else NPM

By default, Quinoa is configured with the commands to call depending on the chosen package manager (to always keep the same behavior and make it easy to switch).

Node packages installation (node_modules)

By default, Quinoa will call the appropriate package manager install command (before building or starting) only if the node_modules directory doesn’t exist.

You may force a new installation using -Dquarkus.quinoa.force-install=true.

Quinoa will use the appropriate package manager frozen-lockfile command when installing, if the environment CI=true, or if quarkus.quinoa.frozen-lockfile=true. In this mode, the lockfile have to be present in the project.

Package manager commands

By default, the following commands and environment variables are used in the different faces for each of the supported package managers.

Install:

  • npm install (npm ci if quarkus.quinoa.frozen-lockfile=true).

  • pnpm install (pnpm install --frozen-lockfile if quarkus.quinoa.frozen-lockfile=true).

  • yarn install (yarn install --frozen-lockfile if quarkus.quinoa.frozen-lockfile=true).

Build:

(npm|pnpm|yarn) run build, with environment MODE=${mode} (dev, test or prod)

Test:

(npm|pnpm|yarn) test, with environment CI=true

Dev:

(npm|pnpm|yarn) start, with environment BROWSER=none

Override package manager commands

By default, Quinoa uses sensible default commands when executing the different phases, install, build, test, dev. It is possible to override one or more of them from the package manager command configuration:

quarkus.quinoa.package-manager-command.install=npm ci --cache $CACHE_DIR/.npm --prefer-offline (1)
quarkus.quinoa.package-manager-command.build-env.BUILD=value (2)
1 This makes npm ci --cache $CACHE_DIR/.npm --prefer-offline the command executed in the install phase. (overriding quarkus.quinoa.package-manager and quarkus.quinoa.frozen-lockfile=true).
2 set environment variable BUILD with value value. Environment variables set in config can be added to the listed commands.
Using custom commands will override quarkus.quinoa.package-manager and quarkus.quinoa.frozen-lockfile.
if NodeJS is installed by Quinoa, you need to enable: quarkus.quinoa.package-manager-command.prepend-binary and adapt the command to only specify the arguments (the binary to call will be prepended by Quinoa).

UI live-coding dev server (proxy mode)

To enable the UI live-coding dev server, set a start script and set the port in the app config. Quinoa will transparently proxy relevant requests to the given port:

quarkus.quinoa.dev-server.port=3000
Quinoa relies on the dev server returning a 404 when the file is not found (See How it works). This is not the case on some dev servers configured with SPA routing. Make sure it is disabled in the dev server configuration (for React Create App, see #91). Another option, when possible, is to use [quarkus-quinoa_quarkus.quinoa.ignored-path-prefixes].

React

App created by Create React App (https://create-react-app.dev/docs/getting-started) are compatible without any change.

To enable React live coding server:

quarkus.quinoa.dev-server.port=3000

Angular

App created by ng (https://angular.io/guide/setup-local) require a tiny bit of configuration:

quarkus.quinoa.build-dir=dist/[your-app-name]

To enable Angular live coding server, you need to edit the package.json start script with ng serve --host 0.0.0.0 --disable-host-check, then add this configuration:

quarkus.quinoa.dev-server.port=4200

If you want to use the Angular tests (instead of Playwright from the @QuarkusTest):

Change the package.json test script:

  "scripts": {
    ...
    "test": "ng test -- --no-watch --no-progress --browsers=ChromeHeadlessCI"
  },

Edit the karma.conf.js:

  browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'],
  customLaunchers: {
    ChromeHeadlessCI: {
      base: 'ChromeHeadless',
      flags: ['--no-sandbox']
    }
},

Next.js

Any app created with Next.js (https://nextjs.org/) should work with Quinoa after the following changes:

In application.properties add:

%dev.quarkus.quinoa.index-page=/
quarkus.quinoa.build-dir=out

In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html".

Add these scripts to package.json

  "scripts": {
    ...
    "start": "next dev",
    "build": "next build && next export",
  }

Vite

Any app created with Vite (https://vitejs.dev/guide/) should work with Quinoa after the following changes:

In application.properties add:

quarkus.quinoa.dev-server.port=5173
quarkus.quinoa.build-dir=dist

Add start script to package.json

  "scripts": {
    ...
    "start": "vite"
  },

Hot Module Replacement (HMR) should work by default.

Single Page application routing

Client-side/Browser/SPA routing is the internal handling of a route from the javascript in the browser. It uses the HTML5 History API

When enabled, to allow SPA routing, all relevant requests will be internally re-routed to index.html, this way the javascript can take care of the route inside the web-application.

To enable Single Page application routing:

quarkus.quinoa.enable-spa-routing=true
By default, Quinoa will ignore quarkus.resteasy-reactive.path, quarkus.resteasy.path and quarkus.http.non-application-root-path path prefixes. You can specify different path prefixes to ignore using quarkus.quinoa.ignored-path-prefixes.
Currently, for technical reasons, the Quinoa SPA routing configuration won’t work with RESTEasy Classic. Instead, you may use a workaround (if your app has all the rest resources under the same path prefix):
@ApplicationScoped
public class SPARouting {
    private static final String[] PATH_PREFIXES = { "/api/", "/q/" };
    private static final Predicate<String> FILE_NAME_PREDICATE = Pattern.compile(".*[.][a-zA-Z\\d]+").asMatchPredicate();

    public void init(@Observes Router router) {
        router.get("/*").handler(rc -> {
            final String path = rc.normalizedPath();
            if (!path.equals("/")
                    && Stream.of(PATH_PREFIXES).noneMatch(path::startsWith)
                    && !FILE_NAME_PREDICATE.test(path)) {
                rc.reroute("/");
            } else {
                rc.next();
            }
        });
    }
}

Http Headers

It’s very common to set up headers for caching on static resources, for example React proposes this configuration:

To configure Quarkus with those headers :

quarkus.http.filter.others.header.Cache-Control=no-cache
quarkus.http.filter.others.matches=/.*
quarkus.http.filter.others.methods=GET
quarkus.http.filter.others.order=0
quarkus.http.filter.static.header.Cache-Control=max-age=31536000
quarkus.http.filter.static.matches=/static/.+
quarkus.http.filter.static.methods=GET
quarkus.http.filter.static.order=1

Http Compression

To enable server Http compression:

quarkus.http.enable-compression=true

Testing

By default, the Web UI is not build/served in @QuarkusTest. The goal is to be able to test your api without having to wait for the Web UI build.

Quinoa features a testing library to make it easier to test your Web UI:

<dependency>
    <groupId>io.quarkiverse.quinoa</groupId>
    <artifactId>quarkus-quinoa-testing</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

In order to enable the Web UI (build and serve) in a particular test, you can use the Enable test profile:

@QuarkusTest
@TestProfile(QuinoaTestProfiles.Enable.class)
public class MyWebUITest {
    @Test
    public void someTest() {
      // your test logic here
    }
}

If you also want to run the tests included in your Web UI (i.e npm test) alongside this class, you can use the EnableAndRunTests test profile:

@QuarkusTest
@TestProfile(QuinoaTestProfiles.EnableAndRunTests.class)
public class AllWebUITest {
    @Test
    public void runTest() {
        // you don't need anything here, it will run your package.json "test"
    }
}

The library also brings a very elegant way to do e2e testing using Playright:

import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Response;
import io.quarkiverse.quinoa.testing.QuarkusPlaywrightManager;
import io.quarkiverse.quinoa.testing.QuinoaTestProfiles;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.net.URL;

@QuarkusTest
@TestProfile(QuinoaTestProfiles.Enable.class)
@QuarkusTestResource(QuarkusPlaywrightManager.class)
public class MyWebUITest {
    @QuarkusPlaywrightManager.InjectPlaywright
    BrowserContext context;

    @TestHTTPResource("/")
    URL url;

    @Test
    void name() {
        final Page page = context.newPage();
        Response response = page.navigate(url.toString());
        Assertions.assertEquals("OK", response.statusText());

        page.waitForLoadState();

        String title = page.title();
        Assertions.assertEquals("My App", title);

        // Make sure the app content is ok
        String greeting = page.innerText(".quinoa");
        Assertions.assertEquals("Hello World", greeting);
    }
}

CI

Most CI images already include NodeJS. if they don’t, just make sure to install it alongside Maven/Gradle (and Yarn/PNPM if needed). Then you can use it like any Maven/Gradle project.

Quinoa can be configured to install packages with a frozen lockfile.

On compatible CIs, don’t forget to enable the Maven/Gradle and NPM/Yarn repository caching.

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

Indicate if the extension should be enabled. Default is true if the Web UI directory exists and dev and prod mode. Default is false in test mode (to avoid building the Web UI during backend tests).

Environment variable: QUARKUS_QUINOA

boolean

disabled in test mode

Indicate if Quinoa should just do the build part. If true, Quinoa will NOT serve the Web UI built resources. This is handy when the output of the build is used to be served via something else (nginx, cdn, …​) Quinoa put the built files in 'target/quinoa-build' (or 'build/quinoa-build with Gradle). Default is false.

Environment variable: QUARKUS_QUINOA_JUST_BUILD

boolean

false

Path to the Web UI (NodeJS) root directory. If not set ${project.root}/src/main/webui/ will be used. otherwise the path will be considered relative to the project root.

Environment variable: QUARKUS_QUINOA_UI_DIR

string

src/main/webui

This the Web UI internal build system (webpack, …​) output directory. After the build, Quinoa will take the files from this directory, move them to 'target/quinoa-build' (or build/quinoa-build with Gradle) and serve them at runtime. The path is relative to the Web UI path. If not set "build/" will be used

Environment variable: QUARKUS_QUINOA_BUILD_DIR

string

build/

Name of the package manager binary. If not set, it will be auto-detected depending on the lockfile falling back to "npm". Only npm, pnpm and yarn are supported for the moment.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER

string

auto-detected with lockfile

Enable Package Manager Installation. This will override "package-manager" config. Set "quarkus.quinoa.package-manager-command.prepend-binary=true" when using with custom commands

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL

boolean

false

The directory where NodeJS should be installed, it will be installed in a node/ sub-directory. Default is ${project.root}/.quinoa

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_INSTALL_DIR

string

.quinoa/

The NodeJS Version to install locally to the project. Required when package-manager-install is enabled.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_VERSION

string

Where to download NodeJS from.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_DOWNLOAD_ROOT

string

https://nodejs.org/dist/

The NPM version to install. By default, the version is provided by NodeJS.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NPM_VERSION

string

provided

Where to download NPM from.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NPM_DOWNLOAD_ROOT

string

https://registry.npmjs.org/npm/-/

The PNPM version to install. If the version is set and NPM and YARN are not set, then this version will attempt to be downloaded.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_VERSION

string

Where to download PNPM from.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_DOWNLOAD_ROOT

string

https://registry.npmjs.org/pnpm/-/

The YARN version to install. If the version is set and NPM Version is not set, then this version will attempt to be downloaded.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_VERSION

string

Where to download YARN from.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_DOWNLOAD_ROOT

string

https://github.com/yarnpkg/yarn/releases/download/

If true, the package manager binary will be prepended by Quinoa (Only configure the arguments in the different commands as the binary will be prepended). e.g. «quarkus.quinoa.package-manager-command.install=ci --cache $CACHE_DIR/.npm --prefer-offline» Else, the command should also contain the binary.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_PREPEND_BINARY

boolean

false

Custom command for installing all dependencies. e.g. «npm ci --cache $CACHE_DIR/.npm --prefer-offline»

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL

string

Custom command for building the application.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD

string

Custom command for running tests for the application.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST

string

Custom command for starting the application in development mode.

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV

string

Name of the index page. If not set, "index.html" will be used.

Environment variable: QUARKUS_QUINOA_INDEX_PAGE

string

index.html

Indicate if the Web UI should also be tested during the build phase (i.e: npm test). To be used in a io.quarkus.test.junit.QuarkusTestProfile to have Web UI test running during a io.quarkus.test.junit.QuarkusTest Default is false.

Environment variable: QUARKUS_QUINOA_RUN_TESTS

boolean

false

Install the packages using a frozen lockfile. Don’t generate a lockfile and fail if an update is needed (useful in CI). If not set it is true if environment CI=true, else it is false.

Environment variable: QUARKUS_QUINOA_FROZEN_LOCKFILE

boolean

true if environment CI=true

Force install packages before building. If not set, it will install packages only if the node_modules directory is absent or when the package.json is modified in dev-mode.

Environment variable: QUARKUS_QUINOA_FORCE_INSTALL

boolean

false

Enable SPA (Single Page Application) routing, all relevant requests will be re-routed to the "index.html". Currently, for technical reasons, the Quinoa SPA routing configuration won’t work with RESTEasy Classic. If not set, it is disabled.

Environment variable: QUARKUS_QUINOA_ENABLE_SPA_ROUTING

boolean

false

List of path prefixes to be ignored by Quinoa. If not set, "quarkus.resteasy-reactive.path", "quarkus.resteasy.path" and "quarkus.http.non-application-root-path" will be ignored.

Environment variable: QUARKUS_QUINOA_IGNORED_PATH_PREFIXES

list of string

Enable external dev server (live coding). The "dev-server.port" config is required to communicate with the dev server. If not set the default is true.

Environment variable: QUARKUS_QUINOA_DEV_SERVER

boolean

true

When set to true, Quinoa will manage the Web UI dev server When set to false, the Web UI dev server have to be started before running Quarkus dev

Environment variable: QUARKUS_QUINOA_DEV_SERVER_MANAGED

boolean

true

Port of the server to forward requests to. The dev server process (i.e npm start) is managed like a dev service by Quarkus. If the external server responds with a 404, it is ignored by Quinoa and processed like any other backend request.

Environment variable: QUARKUS_QUINOA_DEV_SERVER_PORT

int

Host of the server to forward requests to. "localhost" is the default

Environment variable: QUARKUS_QUINOA_DEV_SERVER_HOST

string

localhost

After start, Quinoa wait for the external dev server. by sending GET requests to this path waiting for a 200 status. If not set the default is "/". If empty string "", Quinoa will not check if the dev server is up.

Environment variable: QUARKUS_QUINOA_DEV_SERVER_CHECK_PATH

string

/

By default, Quinoa will handle request upgrade to websocket and act as proxy with the dev server. If set to false, Quinoa will pass websocket upgrade request to the next Vert.x route handler.

Environment variable: QUARKUS_QUINOA_DEV_SERVER_WEBSOCKET

boolean

true

Timeout in ms for the dev server to be up and running. If not set the default is ~30000ms.

Environment variable: QUARKUS_QUINOA_DEV_SERVER_CHECK_TIMEOUT

int

30000

Enable external dev server live coding logs. This is not enabled by default because most dev servers display compilation errors directly in the browser. False if not set.

Environment variable: QUARKUS_QUINOA_DEV_SERVER_LOGS

boolean

false

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV

Map<String,String>

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV

Map<String,String>

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV

Map<String,String>

Environment variable: QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV

Map<String,String>