diff --git a/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java b/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java index f38988f..2d2f213 100644 --- a/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java +++ b/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/WebBundlerConfig.java @@ -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()) diff --git a/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/BundleConfigAssetsBuildItem.java b/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/BundleConfigAssetsBuildItem.java index 093f383..d51ab53 100644 --- a/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/BundleConfigAssetsBuildItem.java +++ b/common-deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/BundleConfigAssetsBuildItem.java @@ -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 webAssets) { diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/BundlingProcessor.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/BundlingProcessor.java index 5b6fc67..a6e20f5 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/BundlingProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/BundlingProcessor.java @@ -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 staticResourceProducer, + BuildProducer generatedBundleProducer, + BuildProducer generatedEntryPointProducer) { try { final long startedBundling = Instant.now().toEpochMilli(); final BundleResult result = Bundler.bundle(readyForBundling.bundleOptions(), false); @@ -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); } diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/DevModeBundlingProcessor.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/DevModeBundlingProcessor.java index 66ccb71..ad123ca 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/DevModeBundlingProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/DevModeBundlingProcessor.java @@ -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; @@ -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; @@ -39,8 +37,6 @@ public class DevModeBundlingProcessor { private static final AtomicReference watchRef = new AtomicReference<>(); private static final AtomicReference bundleExceptionRef = new AtomicReference<>(); - private static final AtomicReference WAITER = new AtomicReference<>(); - private static volatile long lastBundling = 0; @BuildStep(onlyIf = IsDevelopment.class) void watch(WebBundlerConfig config, @@ -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 = () -> { @@ -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()); @@ -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( @@ -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()); @@ -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); } } } diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/PrepareForBundlingProcessor.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/PrepareForBundlingProcessor.java index f37910a..e8021c9 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/PrepareForBundlingProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/PrepareForBundlingProcessor.java @@ -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>>> LOADER_CONFIGS = Map .ofEntries( @@ -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()); @@ -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); @@ -251,22 +259,39 @@ static void createAsset(BuildProducer 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 watchedFiles, WebAsset webAsset, + static void createSymbolicLinkOrFallback(BuildProducer 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 { diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/StaticWebAssetsProcessor.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/StaticWebAssetsProcessor.java index db10f34..85bfa06 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/StaticWebAssetsProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/StaticWebAssetsProcessor.java @@ -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; @@ -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 diff --git a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/ReadyForBundlingBuildItem.java b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/ReadyForBundlingBuildItem.java index e6b1c70..f86dc63 100644 --- a/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/ReadyForBundlingBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/web/bundler/deployment/items/ReadyForBundlingBuildItem.java @@ -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() { @@ -30,4 +33,8 @@ public BundleOptions bundleOptions() { public Path distDir() { return distDir; } + + public boolean enabledBundlingWatch() { + return disableBundlingWatch; + } } diff --git a/deployment/src/test/java/io/quarkiverse/web/bundler/test/WebBundlerDevModeTest.java b/deployment/src/test/java/io/quarkiverse/web/bundler/test/WebBundlerDevModeTest.java index 5a3e0d1..79e0b47 100644 --- a/deployment/src/test/java/io/quarkiverse/web/bundler/test/WebBundlerDevModeTest.java +++ b/deployment/src/test/java/io/quarkiverse/web/bundler/test/WebBundlerDevModeTest.java @@ -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() diff --git a/docs/modules/ROOT/pages/includes/attributes.adoc b/docs/modules/ROOT/pages/includes/attributes.adoc index cb83615..5667035 100644 --- a/docs/modules/ROOT/pages/includes/attributes.adoc +++ b/docs/modules/ROOT/pages/includes/attributes.adoc @@ -1,4 +1,4 @@ -:project-version: 1.6.0 +:project-version: 1.6.1 :maven-version: 3.8.1+ diff --git a/runtime/src/main/java/io/quarkiverse/web/bundler/runtime/devmode/WebBundlerHotReplacementSetup.java b/runtime/src/main/java/io/quarkiverse/web/bundler/runtime/devmode/WebBundlerHotReplacementSetup.java index 48bd788..7c0603f 100644 --- a/runtime/src/main/java/io/quarkiverse/web/bundler/runtime/devmode/WebBundlerHotReplacementSetup.java +++ b/runtime/src/main/java/io/quarkiverse/web/bundler/runtime/devmode/WebBundlerHotReplacementSetup.java @@ -34,16 +34,24 @@ public void setupHotDeployment(HotReplacementContext context) { private void startWatchScheduler() { if (scheduler == null) { + context.addPreScanStep(() -> { + // Make sure we don't scan when another scan is running + if (nextUpdate != Long.MAX_VALUE) { + nextUpdate = System.currentTimeMillis() + 1000; + } + }); scheduler = EXECUTOR.scheduleAtFixedRate(() -> { try { if (context.getDeploymentProblem() == null) { // Let's not scan when there is a deployment problem - // and wait for another request to trigger the scan + // and wait for another request to trigger the scan` if (System.currentTimeMillis() > nextUpdate) { + // Make sure we don't call scan more than once + nextUpdate = Long.MAX_VALUE; if (context.doScan(false)) { - LOGGER.debug("App restarted from watcher, let's wait 5s before watching again"); - // If we have restarted, let's wait 5s before another scan - nextUpdate = System.currentTimeMillis() + 5000; + LOGGER.debug("App restarted from watcher, let's wait 3s before watching again"); + // If we have restarted, let's wait 3s before another scan + nextUpdate = System.currentTimeMillis() + 3000; } else { nextUpdate = System.currentTimeMillis() + 1000; } @@ -51,7 +59,8 @@ private void startWatchScheduler() { } } catch (Exception e) { - throw new IllegalStateException(e); + nextUpdate = System.currentTimeMillis() + 3000; + throw new RuntimeException(e); } }, 500, 500, TimeUnit.MILLISECONDS); } @@ -62,12 +71,13 @@ private void startWatchScheduler() { public void close() { HotReplacementSetup.super.close(); if (scheduler != null) { - scheduler.cancel(false); + scheduler.cancel(true); scheduler = null; } } private void noRestartChanges(Set strings) { + nextUpdate = System.currentTimeMillis() + 1000; for (Consumer> changeEventListener : changeEventListeners) { changeEventListener.accept(strings); }