Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #268/#71: HMR support for web sockets #270

Merged
merged 1 commit into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public class DevServerConfig {
@ConfigItem(name = ConfigItem.PARENT, defaultValue = "true")
public boolean enabled;

/**
* 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
*/
@ConfigItem(defaultValue = "true")
public boolean managed;

/**
* Port of the server to forward requests to.
* The dev server process (i.e npm start) is managed like a dev service by Quarkus.
Expand All @@ -36,6 +43,13 @@ public class DevServerConfig {
@ConfigItem(defaultValue = "/")
public Optional<String> checkPath;

/**
* 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.
*/
@ConfigItem(defaultValue = "true")
public boolean websocket;

/**
* Timeout in ms for the dev server to be up and running.
* If not set the default is ~30000ms.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.deployment.WebsocketSubProtocolsBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;

public class ForwardedDevProcessor {
Expand Down Expand Up @@ -103,22 +104,31 @@ public void run() {
return null;
}

PackageManager packageManager = quinoaDir.get().getPackageManager();
final int devServerPort = quinoaConfig.devServer.port.getAsInt();
final String checkPath = quinoaConfig.devServer.checkPath.orElse(null);
if (!quinoaConfig.devServer.managed) {
if (PackageManager.isDevServerUp(checkPath, devServerPort)) {
return new ForwardedDevServerBuildItem(devServerPort);
} else {
throw new IllegalStateException(
"The Web UI dev server (configured as not managed by Quinoa) is not started on port: " + devServerPort);
}
}

StartupLogCompressor compressor = new StartupLogCompressor(
(launchMode.isTest() ? "(test) " : "") + "Quinoa package manager live coding dev service starting:",
consoleInstalled,
loggingSetup,
PROCESS_THREAD_PREDICATE);

PackageManager packageManager = quinoaDir.get().getPackageManager();
final AtomicReference<Process> dev = new AtomicReference<>();
try {
final int devServerPort = quinoaConfig.devServer.port.getAsInt();
final int checkTimeout = quinoaConfig.devServer.checkTimeout;
if (checkTimeout < 1000) {
throw new ConfigurationException("quarkus.quinoa.dev-server.check-timeout must be greater than 1000ms");
}
final long start = Instant.now().toEpochMilli();
final String checkPath = quinoaConfig.devServer.checkPath.orElse(null);

dev.set(packageManager.dev(devServerPort, checkPath, checkTimeout));
compressor.close();
final LiveCodingLogOutputFilter logOutputFilter = new LiveCodingLogOutputFilter(
Expand Down Expand Up @@ -154,6 +164,7 @@ public void runtimeInit(
Optional<ForwardedDevServerBuildItem> devProxy,
CoreVertxBuildItem vertx,
BuildProducer<RouteBuildItem> routes,
BuildProducer<WebsocketSubProtocolsBuildItem> websocketSubProtocols,
BuildProducer<ResumeOn404BuildItem> resumeOn404) throws IOException {
if (quinoaConfig.justBuild) {
LOG.info("Quinoa is in build only mode");
Expand All @@ -163,8 +174,12 @@ public void runtimeInit(
LOG.infof("Quinoa is forwarding unhandled requests to port: %d", quinoaConfig.devServer.port.getAsInt());
final QuinoaHandlerConfig handlerConfig = quinoaConfig.toHandlerConfig(false, httpBuildTimeConfig);
routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_ROUTE_ORDER)
.handler(recorder.quinoaProxyDevHandler(handlerConfig, vertx.getVertx(), devProxy.get().getPort()))
.handler(recorder.quinoaProxyDevHandler(handlerConfig, vertx.getVertx(), devProxy.get().getPort(),
quinoaConfig.devServer.websocket))
.build());
if (quinoaConfig.devServer.websocket) {
websocketSubProtocols.produce(new WebsocketSubProtocolsBuildItem("*"));
}
if (quinoaConfig.enableSPARouting) {
resumeOn404.produce(new ResumeOn404BuildItem());
routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_SPA_ROUTE_ORDER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public void run() {
}
}

private static boolean isDevServerUp(String path, int port) {
public static boolean isDevServerUp(String path, int port) {
try {
final String normalizedPath = path.indexOf("/") == 0 ? path : "/" + path;
URL url = new URL("http://localhost:" + port + normalizedPath);
Expand Down
32 changes: 32 additions & 0 deletions docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,22 @@ endif::add-copy-button-to-env-var[]
|`true`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.managed]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.managed[quarkus.quinoa.dev-server.managed]`

[.description]
--
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

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_MANAGED+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_QUINOA_DEV_SERVER_MANAGED+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`true`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.port]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.port[quarkus.quinoa.dev-server.port]`

[.description]
Expand Down Expand Up @@ -410,6 +426,22 @@ endif::add-copy-button-to-env-var[]
|`/`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.websocket]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.websocket[quarkus.quinoa.dev-server.websocket]`

[.description]
--
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.

ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_WEBSOCKET+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_QUINOA_DEV_SERVER_WEBSOCKET+++`
endif::add-copy-button-to-env-var[]
--|boolean
|`true`


a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.check-timeout]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.check-timeout[quarkus.quinoa.dev-server.check-timeout]`

[.description]
Expand Down
40 changes: 25 additions & 15 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ App created by `ng` (https://angular.io/guide/setup-local) require a tiny bit of
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 --no-live-reload`, then add this configuration:
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:
[source,properties]
----
quarkus.quinoa.dev-server.port=4200
Expand Down Expand Up @@ -287,6 +287,29 @@ Edit the karma.conf.js:
},
----

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

In application.properties add:
[source,properties]
----
%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
[source,json]
----
"scripts": {
...
"start": "next dev",
"build": "next build && next export",
}
----

[#vite]
=== Vite
Any app created with Vite (https://vitejs.dev/guide/) should work with Quinoa after the following changes:
Expand All @@ -307,20 +330,7 @@ Add start script to package.json
},
----

To make hot module replacement work, add the following to the config object in vite.config.js:
[source,javascript]
----
server: {
port: 5173,
host: '127.0.0.1',
hmr: {
port: 5173,
host: '127.0.0.1',
}
}
----

You do need the host and port in both places to ensure the websocket is constructed and upgraded properly.
Hot Module Replacement (HMR) should work by default.

[#spa-routing]
=== Single Page application routing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
%react-just-build.quarkus.quinoa.just-build=true


#%angular-dev.quarkus.quinoa.dev-server.port=4200
%angular-dev.quarkus.quinoa.dev-server.port=4200
%angular-dev.quarkus.quinoa.ui-dir=src/main/ui-angular
%angular-dev.quarkus.quinoa.build-dir=dist/quinoa-app
%angular-dev.quarkus.quinoa.enable-spa-routing=true
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/src/main/ui-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 --no-live-reload",
"start": "ng serve --host 0.0.0.0 --disable-host-check",
"build": "npm run something && ng build",
"something": "echo \"something\"",
"watch": "ng build --watch --configuration development",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ class QuinoaDevProxyHandler implements Handler<RoutingContext> {
HttpHeaders.CONTENT_TYPE.toString());
private final int port;
private final WebClient client;
private final QuinoaDevWebSocketProxyHandler wsUpgradeHandler;
private final ClassLoader currentClassLoader;
private final QuinoaHandlerConfig config;

QuinoaDevProxyHandler(final QuinoaHandlerConfig config, final Vertx vertx, int port) {
QuinoaDevProxyHandler(final QuinoaHandlerConfig config, final Vertx vertx, int port, boolean websocket) {
this.port = port;
this.client = WebClient.create(vertx);
this.wsUpgradeHandler = websocket ? new QuinoaDevWebSocketProxyHandler(vertx, port) : null;
this.config = config;
currentClassLoader = Thread.currentThread().getContextClassLoader();
}
Expand All @@ -52,14 +54,34 @@ public void handle(final RoutingContext ctx) {
next(currentClassLoader, ctx);
return;
}
final HttpServerRequest request = ctx.request();

final String resourcePath = path.endsWith("/") ? path + config.indexPage : path;
if (isIgnored(resourcePath, config.ignoredPathPrefixes)) {
next(currentClassLoader, ctx);
return;
}
final String uri = computeURI(resourcePath, request);

if (isUpgradeToWebSocket(ctx)) {
if (this.wsUpgradeHandler != null) {
wsUpgradeHandler.handle(ctx);
} else {
next(currentClassLoader, ctx);
}
} else {
handleHttpRequest(ctx, resourcePath);
}
}

private static boolean isUpgradeToWebSocket(RoutingContext ctx) {
return ctx.request().headers().contains("Upgrade")
&& "websocket".equalsIgnoreCase(ctx.request().headers().get("Upgrade"));
}

private void handleHttpRequest(final RoutingContext ctx, final String resourcePath) {
final HttpServerRequest request = ctx.request();
final MultiMap headers = request.headers();
final String uri = computeResourceURI(resourcePath, request);

// Workaround for issue https://github.com/quarkiverse/quarkus-quinoa/issues/91
// See https://www.npmjs.com/package/connect-history-api-fallback#htmlacceptheaders
// When no Accept header is provided, the historyApiFallback is disabled
Expand All @@ -68,26 +90,26 @@ public void handle(final RoutingContext ctx) {
headers.remove("Accept-Encoding");
client.request(request.method(), port, request.localAddress().host(), uri)
.putHeaders(headers)
.send(new Handler<AsyncResult<HttpResponse<Buffer>>>() {
@Override
public void handle(AsyncResult<HttpResponse<Buffer>> event) {
if (event.succeeded()) {
final int statusCode = event.result().statusCode();
if (statusCode == 200) {
.send(event -> {
if (event.succeeded()) {
final int statusCode = event.result().statusCode();
switch (statusCode) {
case 200:
forwardResponse(event, request, ctx, resourcePath);
} else if (statusCode == 404) {
break;
case 404:
next(currentClassLoader, ctx);
} else {
break;
default:
forwardError(event, statusCode, ctx);
}
} else {
error(event, ctx);
}
} else {
error(event, ctx);
}
});
}

private String computeURI(String path, HttpServerRequest request) {
private String computeResourceURI(String path, HttpServerRequest request) {
String uri = path;
final String query = request.query();
if (query != null) {
Expand Down Expand Up @@ -126,8 +148,9 @@ private void forwardResponse(AsyncResult<HttpResponse<Buffer>> event, HttpServer
}

private void error(AsyncResult<HttpResponse<Buffer>> event, RoutingContext ctx) {
final String error = String.format("Quinoa failed to forward request '%s', see logs.", ctx.request().uri());
ctx.response().setStatusCode(500);
ctx.response().send("Quinoa failed to forward request, see logs.");
LOG.error("Quinoa failed to forward request, see logs.", event.cause());
ctx.response().send(error);
LOG.error(error, event.cause());
}
}
Loading