Skip to content

Commit

Permalink
Improve live-reload and fallback when needed
Browse files Browse the repository at this point in the history
  • Loading branch information
ia3andy committed Jul 16, 2024
1 parent 376f475 commit 57c7860
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,6 @@ static boolean isEqual(WebBundlerConfig c1, WebBundlerConfig c2) {
}

return Objects.equals(c1.webRoot(), c2.webRoot())
&& Objects.equals(c1.bundle(), c2.bundle())
&& Objects.equals(c1.staticDir(), c2.staticDir())
&& Objects.equals(c1.bundlePath(), c2.bundlePath())
&& BundlingConfig.isEqual(c1.bundling(), c2.bundling())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import java.util.List;

/**
* This contains config for bundling such as tsconfig.json
*/
public final class BundleConfigAssetsBuildItem extends WebAssetsBuildItem {

public BundleConfigAssetsBuildItem(List<WebAsset> webAssets) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ void bundle(WebBundlerConfig config,
if (readyForBundling == null) {
return;
}
bundleAndProcess(config, readyForBundling, staticResourceProducer, generatedBundleProducer,
generatedEntryPointProducer);
}

static BundleResult bundleAndProcess(WebBundlerConfig config, ReadyForBundlingBuildItem readyForBundling,
BuildProducer<GeneratedWebResourceBuildItem> staticResourceProducer,
BuildProducer<GeneratedBundleBuildItem> generatedBundleProducer,
BuildProducer<GeneratedEntryPointBuildItem> generatedEntryPointProducer) {
try {
final long startedBundling = Instant.now().toEpochMilli();
final BundleResult result = Bundler.bundle(readyForBundling.bundleOptions(), false);
Expand All @@ -55,6 +63,7 @@ void bundle(WebBundlerConfig config,

handleBundleDistDir(config, generatedBundleProducer, staticResourceProducer, result.dist(), startedBundling);
processGeneratedEntryPoints(config, readyForBundling.bundleOptions().workDir(), generatedEntryPointProducer);
return result;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package io.quarkiverse.web.bundler.deployment;

import static io.quarkiverse.web.bundler.deployment.BundlingProcessor.handleBundleDistDir;
import static io.quarkiverse.web.bundler.deployment.BundlingProcessor.processGeneratedEntryPoints;
import static io.quarkiverse.web.bundler.deployment.BundlingProcessor.*;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import org.jboss.logging.Logger;
Expand All @@ -18,6 +15,7 @@
import io.mvnpm.esbuild.Bundler;
import io.mvnpm.esbuild.Watch;
import io.mvnpm.esbuild.model.BundleOptions;
import io.mvnpm.esbuild.model.BundleResult;
import io.quarkiverse.web.bundler.deployment.items.GeneratedBundleBuildItem;
import io.quarkiverse.web.bundler.deployment.items.GeneratedEntryPointBuildItem;
import io.quarkiverse.web.bundler.deployment.items.ReadyForBundlingBuildItem;
Expand All @@ -39,8 +37,6 @@ public class DevModeBundlingProcessor {
private static final AtomicReference<Watch> watchRef = new AtomicReference<>();

private static final AtomicReference<BundleException> bundleExceptionRef = new AtomicReference<>();
private static final AtomicReference<CountDownLatch> WAITER = new AtomicReference<>();
private static volatile long lastBundling = 0;

@BuildStep(onlyIf = IsDevelopment.class)
void watch(WebBundlerConfig config,
Expand All @@ -57,40 +53,40 @@ void watch(WebBundlerConfig config,
final BundlesBuildContext bundlesBuildContext = liveReload.getContextObject(BundlesBuildContext.class);
final boolean isLiveReload = liveReload.isLiveReload();
Watch watch = DevModeBundlingProcessor.watchRef.get();
if (isLiveReload && devService != null) {
boolean shouldShutdownTheBroker = bundlesBuildContext == null
|| watch == null
|| !watch.isAlive()
|| !WebBundlerConfig.isEqual(config, bundlesBuildContext.config());
if (!shouldShutdownTheBroker) {
try {
if (readyForBundling.started() == null) {
watch.updateEntries(readyForBundling.bundleOptions().entries());
// We should just wait for the change to happen
waitForBundling(readyForBundling);
}
if (readyForBundling.started() == null) {
// no changes
boolean isRestartWatchNeeded = readyForBundling.enabledBundlingWatch() && (watch == null || !watch.isAlive());
if (!isRestartWatchNeeded) {
if (watch != null && watch.isAlive()) {
devServices.produce(devService.toBuildItem());
final BundlesBuildContext newBundlesBuildContext = new BundlesBuildContext(readyForBundling.bundleOptions(),
config, watch.dist());
liveReload.setContextObject(BundlesBuildContext.class, newBundlesBuildContext);
handleBundleDistDir(config, generatedBundleProducer, staticResourceProducer, watch.dist(),
readyForBundling.started());
processGeneratedEntryPoints(config, readyForBundling.bundleOptions().workDir(),
generatedEntryPointProducer);
} catch (IOException e) {
shutdownDevService();
liveReload.setContextObject(BundlesBuildContext.class, new BundlesBuildContext());
throw new UncheckedIOException(e);
} catch (Exception e) {
shutdownDevService();
liveReload.setContextObject(BundlesBuildContext.class, new BundlesBuildContext());
throw e;
}
final BundlesBuildContext newBundlesBuildContext = new BundlesBuildContext(readyForBundling.bundleOptions(),
bundlesBuildContext.bundleDistDir());
liveReload.setContextObject(BundlesBuildContext.class, newBundlesBuildContext);
handleBundleDistDir(config, generatedBundleProducer, staticResourceProducer,
bundlesBuildContext.bundleDistDir(),
readyForBundling.started());
processGeneratedEntryPoints(config, readyForBundling.bundleOptions().workDir(),
generatedEntryPointProducer);
return;
}
}

if (watch != null) {
shutdownDevService();
}

if (!readyForBundling.enabledBundlingWatch()) {
// We use normal bundling when watch is not enabled
final BundleResult bundleResult = bundleAndProcess(config, readyForBundling, staticResourceProducer,
generatedBundleProducer,
generatedEntryPointProducer);
final BundlesBuildContext newBundlesBuildContext = new BundlesBuildContext(readyForBundling.bundleOptions(),
bundleResult.dist());
liveReload.setContextObject(BundlesBuildContext.class, newBundlesBuildContext);
return;
}

if (!isLiveReload) {
// Only the first time
Runnable closeTask = () -> {
Expand All @@ -108,7 +104,6 @@ void watch(WebBundlerConfig config,
return;
}
LOGGER.debugf("New bundling event received: %s", r);
lastBundling = Instant.now().toEpochMilli();
if (!r.isSuccess()) {
bundleExceptionRef.set(r.bundleException());
RuntimeUpdatesProcessor.INSTANCE.setRemoteProblem(r.bundleException());
Expand All @@ -120,10 +115,6 @@ void watch(WebBundlerConfig config,
shutdownDevService();
}
callNoRestartChangesConsumers(r.isSuccess());
final CountDownLatch countDownLatch = WAITER.get();
if (countDownLatch != null) {
countDownLatch.countDown();
}
}, false);
watchRef.set(watch);
devService = new DevServicesResultBuildItem.RunningDevService(
Expand All @@ -133,7 +124,7 @@ void watch(WebBundlerConfig config,
throw watch.firstBuildResult().bundleException();
}
final BundlesBuildContext newBundlesBuildContext = new BundlesBuildContext(readyForBundling.bundleOptions(),
config, watch.dist());
watch.dist());
liveReload.setContextObject(BundlesBuildContext.class, newBundlesBuildContext);
handleBundleDistDir(config, generatedBundleProducer, staticResourceProducer, watch.dist(),
readyForBundling.started());
Expand Down Expand Up @@ -176,46 +167,11 @@ private void shutdownDevService() {
}
}

private void waitForBundling(ReadyForBundlingBuildItem readyForBundling) {
if (readyForBundling.started() != null) {
if (lastBundling > readyForBundling.started()) {
LOGGER.debug("Bundling done, no need to wait");
} else {
final CountDownLatch latch = new CountDownLatch(1);
final CountDownLatch existingLatch = WAITER.getAndSet(latch);
WAITER.set(latch);
LOGGER.info("Bundling started...");
try {
latch.await();
LOGGER.debug("Bundling completed!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
shutdownDevService();
throw new RuntimeException(e);
} finally {
if (existingLatch != null) {
// this will always countdown the previous latch,
// it has no effect if it was already done
existingLatch.countDown();
}
}
}
}

final BundleException bundleException = bundleExceptionRef.get();
if (bundleException != null) {
shutdownDevService();
throw bundleException;
}

}

record BundlesBuildContext(BundleOptions bundleOptions,
WebBundlerConfig config,
Path bundleDistDir) {

public BundlesBuildContext() {
this(null, null, null);
this(null, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
public class PrepareForBundlingProcessor {

private static final Logger LOGGER = Logger.getLogger(PrepareForBundlingProcessor.class);
public static volatile boolean enableBundlingWatch = true;

private static final Map<EsBuildConfig.Loader, Function<LoadersConfig, Optional<Set<String>>>> LOADER_CONFIGS = Map
.ofEntries(
Expand Down Expand Up @@ -120,20 +121,27 @@ ReadyForBundlingBuildItem prepareForBundling(WebBundlerConfig config,
&& WebBundlerConfig.isEqual(config, prepareForBundlingContext.config())
&& Objects.equals(installedWebDependencies.list(), prepareForBundlingContext.dependencies())
&& !liveReload.getChangedResources().contains(config.fromWebRoot("tsconfig.json"))
&& entryPoints.equals(prepareForBundlingContext.entryPoints())) {
// We need to set non-restart watched file again
for (EntryPointBuildItem entryPoint : entryPoints) {
for (BundleWebAsset webAsset : entryPoint.getWebAssets()) {
if (webAsset.isFile() && config.browserLiveReload()) {
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setRestartNeeded(webAsset.srcFilePath().isEmpty())
.setLocation(webAsset.resourceName())
.build());
&& entryPoints.equals(prepareForBundlingContext.entryPoints())
&& entryPoints.stream().map(EntryPointBuildItem::getWebAssets).flatMap(List::stream)
.map(WebAsset::resourceName)
.noneMatch(liveReload.getChangedResources()::contains)) {
if (config.browserLiveReload() && enableBundlingWatch) {
// We need to set non-restart watched file again
for (EntryPointBuildItem entryPoint : entryPoints) {
for (BundleWebAsset webAsset : entryPoint.getWebAssets()) {
if (webAsset.isFile() && webAsset.srcFilePath().isPresent()) {
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setRestartNeeded(false)
.setLocation(webAsset.resourceName())
.build());
}
}
}
}
return new ReadyForBundlingBuildItem(prepareForBundlingContext.bundleOptions(), null, targetDir.dist());
return new ReadyForBundlingBuildItem(prepareForBundlingContext.bundleOptions(), null, targetDir.dist(),
enableBundlingWatch);
}
enableBundlingWatch = true;

try {
Files.createDirectories(targetDir.webBundler());
Expand Down Expand Up @@ -237,7 +245,7 @@ ReadyForBundlingBuildItem prepareForBundling(WebBundlerConfig config,
final BundleOptions options = optionsBuilder.build();
liveReload.setContextObject(PrepareForBundlingContext.class,
new PrepareForBundlingContext(config, installedWebDependencies.list(), entryPoints, options));
return new ReadyForBundlingBuildItem(options, started, targetDir.dist());
return new ReadyForBundlingBuildItem(options, started, targetDir.dist(), enableBundlingWatch);
} catch (IOException e) {
liveReload.setContextObject(PrepareForBundlingContext.class, new PrepareForBundlingContext());
throw new UncheckedIOException(e);
Expand All @@ -251,22 +259,39 @@ static void createAsset(BuildProducer<HotDeploymentWatchedFileBuildItem> watched
Files.write(targetPath, webAsset.resource().content(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
} else {
if (browserLiveReload) {
createSymbolicLink(watchedFiles, webAsset, targetPath);
if (browserLiveReload && enableBundlingWatch) {
createSymbolicLinkOrFallback(watchedFiles, webAsset, targetPath);
} else {
Files.copy(webAsset.resource().path(), targetPath, StandardCopyOption.REPLACE_EXISTING);
}
}
}

static void createSymbolicLink(BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles, WebAsset webAsset,
static void createSymbolicLinkOrFallback(BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles, WebAsset webAsset,
Path targetPath) throws IOException {
Files.deleteIfExists(targetPath);
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setRestartNeeded(webAsset.srcFilePath().isEmpty())
.setLocation(webAsset.resourceName())
.build());
Files.createSymbolicLink(targetPath, webAsset.srcFilePath().orElse(webAsset.resource().path()));
final boolean srcDetected = webAsset.srcFilePath().isPresent();
if (srcDetected) {
try {
Files.createSymbolicLink(targetPath, webAsset.srcFilePath().get());
// the default is restart for all web files, let's disable restart for this one.
// it will be detected by esbuild watcher
watchedFiles.produce(HotDeploymentWatchedFileBuildItem.builder()
.setRestartNeeded(false)
.setLocation(webAsset.resourceName())
.build());
} catch (UnsupportedOperationException e) {
enableBundlingWatch = false;
LOGGER.warn(
"Creating a symbolic link was not authorized on this system. It is required by the Web Bundler to allow filesystem watch. As a result, Web Bundler live-reload will use a scheduler as a fallback.\n\nTo resolve this issue, please add the necessary permissions to allow symbolic link creation.");
Files.copy(webAsset.resource().path(), targetPath, StandardCopyOption.REPLACE_EXISTING);
}
} else {
LOGGER.warn(
"The sources are necessary by the Web Bundler to allow filesystem watch. Web Bundler live-reload will use a scheduler as a fallback");
enableBundlingWatch = false;
Files.copy(webAsset.resource().path(), targetPath, StandardCopyOption.REPLACE_EXISTING);
}
}

private byte[] readLiveReloadJs() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.quarkiverse.web.bundler.deployment;

import static io.quarkiverse.web.bundler.deployment.PrepareForBundlingProcessor.createSymbolicLink;
import static io.quarkiverse.web.bundler.deployment.PrepareForBundlingProcessor.createSymbolicLinkOrFallback;
import static io.quarkiverse.web.bundler.deployment.util.PathUtils.prefixWithSlash;

import java.io.IOException;
Expand Down Expand Up @@ -39,7 +39,7 @@ void processStaticWebAssets(WebBundlerConfig config,
} else {
if (browserLiveReload) {
Files.createDirectories(targetPath.getParent());
createSymbolicLink(watchedFileBuildItemProducer, webAsset, targetPath);
createSymbolicLinkOrFallback(watchedFileBuildItemProducer, webAsset, targetPath);
makePublic(staticResourceProducer, prefixWithSlash(publicPath), targetPath, SourceType.STATIC_ASSET);
} else {
// We can read the file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ public final class ReadyForBundlingBuildItem extends SimpleBuildItem {

private final Path distDir;

public ReadyForBundlingBuildItem(BundleOptions bundleOptions, Long started, Path distDir) {
private final boolean disableBundlingWatch;

public ReadyForBundlingBuildItem(BundleOptions bundleOptions, Long started, Path distDir, boolean disableBundlingWatch) {
this.bundleOptions = bundleOptions;
this.started = started;
this.distDir = distDir;
this.disableBundlingWatch = disableBundlingWatch;
}

public Long started() {
Expand All @@ -30,4 +33,8 @@ public BundleOptions bundleOptions() {
public Path distDir() {
return distDir;
}

public boolean enabledBundlingWatch() {
return disableBundlingWatch;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public void test() throws InterruptedException {
.body(Matchers.containsString("console.log(\"Hello World! Modified!\");"));
test.modifyResourceFile("web/app/app.css", s -> s.replace("background-color: #6b6bf5;", "background-color: #123456;"));
test.modifyResourceFile("web/app/other.scss", s -> s.replace("color: #AAAAAA;", "color: #567890;"));
Thread.sleep(1000);
RestAssured.given()
.get("/foo/bar/static/bundle/main.css")
.then()
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
:project-version: 1.6.0
:project-version: 1.6.1

:maven-version: 3.8.1+

Expand Down
Loading

0 comments on commit 57c7860

Please sign in to comment.