diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/DevServerConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/DevServerConfig.java deleted file mode 100644 index 52c407c5..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/DevServerConfig.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.quarkiverse.quinoa.deployment; - -import java.util.Objects; -import java.util.Optional; -import java.util.OptionalInt; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup -public class DevServerConfig { - - /** - * 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. - */ - @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. - * If the external server responds with a 404, it is ignored by Quinoa and processed like any other backend request. - */ - @ConfigItem - public OptionalInt port; - - /** - * Host of the server to forward requests to. - * "localhost" is the default - */ - @ConfigItem(defaultValue = "localhost") - public String host; - - /** - * 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. - */ - @ConfigItem(defaultValue = "/") - public Optional 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. - */ - @ConfigItem(defaultValue = "30000") - public int checkTimeout; - - /** - * 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. - */ - @ConfigItem - public boolean logs; - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - DevServerConfig that = (DevServerConfig) o; - return checkTimeout == that.checkTimeout && logs == that.logs && Objects.equals(enabled, that.enabled) - && Objects.equals(port, that.port) && Objects.equals(checkPath, that.checkPath); - } - - @Override - public int hashCode() { - return Objects.hash(enabled, port, checkPath, checkTimeout, logs); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java index d1acb345..55039c58 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevProcessor.java @@ -2,6 +2,9 @@ import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_ROUTE_ORDER; import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_SPA_ROUTE_ORDER; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isDevServerMode; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.toHandlerConfig; +import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner.DEV_PROCESS_THREAD_PREDICATE; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.dev.testing.MessageFormat.RESET; import static java.lang.String.join; @@ -22,13 +25,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiPredicate; -import java.util.function.Predicate; import org.jboss.logging.Logger; import io.quarkiverse.quinoa.QuinoaHandlerConfig; import io.quarkiverse.quinoa.QuinoaRecorder; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManager; +import io.quarkiverse.quinoa.deployment.config.DevServerConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.items.ConfiguredQuinoaBuildItem; +import io.quarkiverse.quinoa.deployment.items.ForwardedDevServerBuildItem; +import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -38,7 +44,6 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.console.ConsoleInstalledBuildItem; -import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.dev.console.QuarkusConsole; import io.quarkus.resteasy.reactive.server.spi.ResumeOn404BuildItem; @@ -51,44 +56,42 @@ public class ForwardedDevProcessor { private static final Logger LOG = Logger.getLogger(ForwardedDevProcessor.class); - private static final Predicate PROCESS_THREAD_PREDICATE = thread -> thread.getName() - .matches("Process (stdout|stderr) streamer"); private static final String DEV_SERVICE_NAME = "quinoa-dev-server"; private static volatile DevServicesResultBuildItem.RunningDevService devService; @BuildStep(onlyIf = IsDevelopment.class) public ForwardedDevServerBuildItem prepareDevService( - QuinoaConfig quinoaConfig, LaunchModeBuildItem launchMode, - Optional quinoaDir, + ConfiguredQuinoaBuildItem configuredQuinoa, + QuinoaConfig userConfig, BuildProducer devServices, Optional consoleInstalled, LoggingSetupBuildItem loggingSetup, CuratedApplicationShutdownBuildItem shutdown, LiveReloadBuildItem liveReload) { - if (quinoaDir.isEmpty()) { + if (configuredQuinoa == null) { return null; } - final QuinoaDirectoryBuildItem quinoaDirectoryBuildItem = quinoaDir.get(); QuinoaConfig oldConfig = liveReload.getContextObject(QuinoaConfig.class); - liveReload.setContextObject(QuinoaConfig.class, quinoaConfig); - final String configuredDevServerHost = quinoaConfig.devServer.host; - final String devServerCommand = quinoaDirectoryBuildItem.getDevServerCommand(); - final PackageManager packageManager = quinoaDir.get().getPackageManager(); - final String checkPath = quinoaConfig.devServer.checkPath.orElse(null); + final QuinoaConfig resolvedConfig = configuredQuinoa.resolvedConfig(); + final DevServerConfig devServerConfig = resolvedConfig.devServer(); + liveReload.setContextObject(QuinoaConfig.class, resolvedConfig); + final String configuredDevServerHost = devServerConfig.host(); + final PackageManagerRunner packageManagerRunner = configuredQuinoa.getPackageManager(); + final String checkPath = resolvedConfig.devServer().checkPath().orElse(null); if (devService != null) { - boolean shouldShutdownTheBroker = !quinoaConfig.equals(oldConfig); + boolean shouldShutdownTheBroker = !resolvedConfig.equals(oldConfig); if (!shouldShutdownTheBroker) { - if (quinoaDirectoryBuildItem.getDevServerPort().isEmpty()) { + if (devServerConfig.port().isEmpty()) { throw new IllegalStateException( "Quinoa package manager live coding shouldn't running with an empty the dev-server.port"); } LOG.debug("Quinoa config did not change; no need to restart."); devServices.produce(devService.toBuildItem()); - final int devServerPort = quinoaDirectoryBuildItem.getDevServerPort().getAsInt(); - final String resolvedDevServerHost = PackageManager.isDevServerUp(configuredDevServerHost, devServerPort, + final String resolvedDevServerHost = PackageManagerRunner.isDevServerUp(devServerConfig.host(), + devServerConfig.port().get(), checkPath); - return new ForwardedDevServerBuildItem(resolvedDevServerHost, devServerPort); + return new ForwardedDevServerBuildItem(resolvedDevServerHost, devServerConfig.port().get()); } shutdownDevService(); } @@ -103,105 +106,104 @@ public ForwardedDevServerBuildItem prepareDevService( shutdown.addCloseTask(closeTask, true); } - if (!quinoaDirectoryBuildItem.isDevServerMode(quinoaConfig.devServer)) { + if (!isDevServerMode(configuredQuinoa.resolvedConfig())) { return null; } + final Integer port = devServerConfig.port().get(); - final int devServerPort = quinoaDirectoryBuildItem.getDevServerPort().getAsInt(); - if (!quinoaConfig.devServer.managed) { + if (!devServerConfig.managed()) { // No need to start the dev-service it is not managed by Quinoa // We just check that it is up - final String hostAddress = PackageManager.isDevServerUp(configuredDevServerHost, devServerPort, checkPath); - if (hostAddress != null) { - return new ForwardedDevServerBuildItem(hostAddress, devServerPort); + final String resolvedHostAddress = PackageManagerRunner.isDevServerUp(configuredDevServerHost, port, checkPath); + if (resolvedHostAddress != null) { + return new ForwardedDevServerBuildItem(resolvedHostAddress, port); } else { throw new IllegalStateException( - "The Web UI dev server (configured as not managed by Quinoa) is not started on port: " + devServerPort); + "The Web UI dev server (configured as not managed by Quinoa) is not started on port: " + port); } } - StartupLogCompressor compressor = new StartupLogCompressor( - (launchMode.isTest() ? "(test) " : "") + "Quinoa package manager live coding dev service starting:", - consoleInstalled, - loggingSetup, - PROCESS_THREAD_PREDICATE); + final int checkTimeout = devServerConfig.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 AtomicReference dev = new AtomicReference<>(); + PackageManagerRunner.DevServer devServer = null; try { - 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 PackageManager.DevServer devServer = packageManager.dev(devServerCommand, configuredDevServerHost, - devServerPort, - checkPath, checkTimeout); + devServer = packageManagerRunner.dev(consoleInstalled, loggingSetup, configuredDevServerHost, + port, + checkPath, + checkTimeout); dev.set(devServer.process()); - compressor.close(); + devServer.logCompressor().close(); final LiveCodingLogOutputFilter logOutputFilter = new LiveCodingLogOutputFilter( - quinoaConfig.devServer.logs); + devServerConfig.logs()); if (checkPath != null) { LOG.infof("Quinoa package manager live coding is up and running on port: %d (in %dms)", - devServerPort, Instant.now().toEpochMilli() - start); + port, Instant.now().toEpochMilli() - start); } final Closeable onClose = () -> { logOutputFilter.close(); - packageManager.stopDev(dev.get()); + packageManagerRunner.stopDev(dev.get()); }; - Map devServerConfigMap = createDevServiceMapForDevUI(quinoaConfig, devServer.hostAddress(), - devServerPort, checkPath); + Map devServerConfigMap = createDevServiceMapForDevUI(userConfig); devService = new DevServicesResultBuildItem.RunningDevService( DEV_SERVICE_NAME, null, onClose, devServerConfigMap); devServices.produce(devService.toBuildItem()); - return new ForwardedDevServerBuildItem(devServer.hostAddress(), devServerPort); + return new ForwardedDevServerBuildItem(devServer.hostAddress(), port); } catch (Throwable t) { - packageManager.stopDev(dev.get()); - compressor.closeAndDumpCaptured(); + packageManagerRunner.stopDev(dev.get()); + if (devServer != null) { + devServer.logCompressor().closeAndDumpCaptured(); + } throw new RuntimeException(t); } } - private static Map createDevServiceMapForDevUI(QuinoaConfig quinoaConfig, String devServerHost, - int devServerPort, String checkPath) { + private static Map createDevServiceMapForDevUI(QuinoaConfig quinoaConfig) { Map devServerConfigMap = new LinkedHashMap<>(); - devServerConfigMap.put("quarkus.quinoa.dev-server.host", devServerHost); - devServerConfigMap.put("quarkus.quinoa.dev-server.port", Integer.toString(devServerPort)); + devServerConfigMap.put("quarkus.quinoa.dev-server.host", quinoaConfig.devServer().host()); + devServerConfigMap.put("quarkus.quinoa.dev-server.port", + quinoaConfig.devServer().port().map(p -> p.toString()).orElse("")); devServerConfigMap.put("quarkus.quinoa.dev-server.check-timeout", - Integer.toString(quinoaConfig.devServer.checkTimeout)); - devServerConfigMap.put("quarkus.quinoa.dev-server.check-path", checkPath); - devServerConfigMap.put("quarkus.quinoa.dev-server.managed", Boolean.toString(quinoaConfig.devServer.managed)); - devServerConfigMap.put("quarkus.quinoa.dev-server.logs", Boolean.toString(quinoaConfig.devServer.logs)); - devServerConfigMap.put("quarkus.quinoa.dev-server.websocket", Boolean.toString(quinoaConfig.devServer.websocket)); + Integer.toString(quinoaConfig.devServer().checkTimeout())); + devServerConfigMap.put("quarkus.quinoa.dev-server.check-path", quinoaConfig.devServer().checkPath().orElse("")); + devServerConfigMap.put("quarkus.quinoa.dev-server.managed", Boolean.toString(quinoaConfig.devServer().managed())); + devServerConfigMap.put("quarkus.quinoa.dev-server.logs", Boolean.toString(quinoaConfig.devServer().logs())); + devServerConfigMap.put("quarkus.quinoa.dev-server.websocket", Boolean.toString(quinoaConfig.devServer().websocket())); return devServerConfigMap; } @BuildStep(onlyIf = IsDevelopment.class) @Record(RUNTIME_INIT) public void runtimeInit( - QuinoaConfig quinoaConfig, QuinoaRecorder recorder, HttpBuildTimeConfig httpBuildTimeConfig, Optional devProxy, + Optional configuredQuinoa, CoreVertxBuildItem vertx, BuildProducer routes, BuildProducer websocketSubProtocols, BuildProducer resumeOn404) throws IOException { - if (quinoaConfig.justBuild) { - LOG.info("Quinoa is in build only mode"); - return; - } - if (quinoaConfig.isEnabled() && devProxy.isPresent()) { + + if (configuredQuinoa.isPresent() && devProxy.isPresent()) { + final QuinoaConfig quinoaConfig = configuredQuinoa.get().resolvedConfig(); + if (quinoaConfig.justBuild()) { + LOG.info("Quinoa is in build only mode"); + return; + } LOG.infof("Quinoa is forwarding unhandled requests to port: %d", devProxy.get().getPort()); - final QuinoaHandlerConfig handlerConfig = quinoaConfig.toHandlerConfig(false, httpBuildTimeConfig); + final QuinoaHandlerConfig handlerConfig = toHandlerConfig(quinoaConfig, false, httpBuildTimeConfig); routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_ROUTE_ORDER) .handler(recorder.quinoaProxyDevHandler(handlerConfig, vertx.getVertx(), devProxy.get().getHost(), devProxy.get().getPort(), - quinoaConfig.devServer.websocket)) + quinoaConfig.devServer().websocket())) .build()); - if (quinoaConfig.devServer.websocket) { + if (quinoaConfig.devServer().websocket()) { websocketSubProtocols.produce(new WebsocketSubProtocolsBuildItem("*")); } - if (quinoaConfig.enableSPARouting) { + if (quinoaConfig.enableSPARouting()) { resumeOn404.produce(new ResumeOn404BuildItem()); routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_SPA_ROUTE_ORDER) .handler(recorder.quinoaSPARoutingHandler(handlerConfig)) @@ -256,7 +258,7 @@ public void close() { @Override public boolean test(final String s, final Boolean err) { Thread current = Thread.currentThread(); - if (PROCESS_THREAD_PREDICATE.test(current) && !err) { + if (DEV_PROCESS_THREAD_PREDICATE.test(current) && !err) { if (!enableLogs) { return false; } diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaConfig.java deleted file mode 100644 index ae1a423d..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaConfig.java +++ /dev/null @@ -1,188 +0,0 @@ -package io.quarkiverse.quinoa.deployment; - -import static java.util.stream.Collectors.toList; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - -import io.quarkiverse.quinoa.QuinoaHandlerConfig; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerCommandConfig; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstallConfig; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; -import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; - -@ConfigRoot(phase = ConfigPhase.BUILD_TIME) -public class QuinoaConfig { - - public static final String DEFAULT_BUILD_DIR = "build/"; - private static final String DEFAULT_WEB_UI_DIR = "src/main/webui"; - private static final String DEFAULT_INDEX_PAGE = "index.html"; - - /** - * 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). - */ - @ConfigItem(name = ConfigItem.PARENT, defaultValueDocumentation = "disabled in test mode") - Optional enable; - - /** - * 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. - */ - @ConfigItem - public boolean justBuild; - - /** - * 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. - */ - @ConfigItem(defaultValue = DEFAULT_WEB_UI_DIR) - public String uiDir; - - /** - * 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 - */ - @ConfigItem(defaultValue = DEFAULT_BUILD_DIR) - public String buildDir; - - /** - * 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. - */ - @ConfigItem(defaultValueDocumentation = "auto-detected with lockfile") - public Optional packageManager; - - /** - * Configuration for installing the package manager - */ - @ConfigItem - public PackageManagerInstallConfig packageManagerInstall; - - /** - * Configuration for overriding build commands - */ - @ConfigItem - public PackageManagerCommandConfig packageManagerCommand; - - /** - * Name of the index page. - * If not set, "index.html" will be used. - */ - @ConfigItem(defaultValue = DEFAULT_INDEX_PAGE) - public String indexPage; - - /** - * Indicate if the Web UI should also be tested during the build phase (i.e: npm test). - * To be used in a {@link io.quarkus.test.junit.QuarkusTestProfile} to have Web UI test running during a - * {@link io.quarkus.test.junit.QuarkusTest} - * Default is false. - */ - @ConfigItem(name = "run-tests") - boolean runTests; - - /** - * 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. - */ - @ConfigItem(defaultValueDocumentation = "true if environment CI=true") - public Optional frozenLockfile; - - /** - * 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. - */ - @ConfigItem - public boolean forceInstall; - - /** - * 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. - */ - @ConfigItem - public boolean enableSPARouting; - - /** - * 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. - */ - @ConfigItem - public Optional> ignoredPathPrefixes; - - /** - * Configuration for the external dev server (live coding server) - */ - @ConfigItem - public DevServerConfig devServer; - - public List getNormalizedIgnoredPathPrefixes() { - return ignoredPathPrefixes.orElseGet(() -> { - Config config = ConfigProvider.getConfig(); - List defaultIgnore = new ArrayList<>(); - readExternalConfigPath(config, "quarkus.resteasy.path").ifPresent(defaultIgnore::add); - readExternalConfigPath(config, "quarkus.resteasy-reactive.path").ifPresent(defaultIgnore::add); - readExternalConfigPath(config, "quarkus.http.non-application-root-path").ifPresent(defaultIgnore::add); - return defaultIgnore; - }).stream().map(s -> s.startsWith("/") ? s : "/" + s).collect(toList()); - } - - public QuinoaHandlerConfig toHandlerConfig(boolean prodMode, final HttpBuildTimeConfig httpBuildTimeConfig) { - final Set compressMediaTypes = httpBuildTimeConfig.compressMediaTypes.map(Set::copyOf).orElse(Set.of()); - return new QuinoaHandlerConfig(getNormalizedIgnoredPathPrefixes(), indexPage, prodMode, - httpBuildTimeConfig.enableCompression, compressMediaTypes); - } - - private Optional readExternalConfigPath(Config config, String key) { - return config.getOptionalValue(key, String.class) - .filter(s -> !Objects.equals(s, "/")) - .map(s -> s.endsWith("/") ? s : s + "/"); - } - - public boolean isEnabled() { - return enable.orElse(true); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - QuinoaConfig that = (QuinoaConfig) o; - return runTests == that.runTests && forceInstall == that.forceInstall && enableSPARouting == that.enableSPARouting - && Objects.equals(enable, that.enable) && Objects.equals(uiDir, that.uiDir) - && Objects.equals(buildDir, that.buildDir) && Objects.equals(packageManager, that.packageManager) - && Objects.equals(packageManagerInstall, that.packageManagerInstall) - && Objects.equals(packageManagerCommand, that.packageManagerCommand) - && Objects.equals(indexPage, that.indexPage) && Objects.equals(frozenLockfile, that.frozenLockfile) - && Objects.equals(ignoredPathPrefixes, that.ignoredPathPrefixes) && Objects.equals(devServer, that.devServer); - } - - @Override - public int hashCode() { - return Objects.hash(enable, uiDir, buildDir, packageManager, packageManagerInstall, packageManagerCommand, indexPage, - runTests, frozenLockfile, forceInstall, enableSPARouting, ignoredPathPrefixes, devServer); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaDirectoryBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaDirectoryBuildItem.java deleted file mode 100644 index 0a26b705..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaDirectoryBuildItem.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.quarkiverse.quinoa.deployment; - -import java.nio.file.Path; -import java.util.Objects; -import java.util.OptionalInt; - -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManager; -import io.quarkus.builder.item.SimpleBuildItem; - -public final class QuinoaDirectoryBuildItem extends SimpleBuildItem { - - private final PackageManager packageManager; - - /** - * 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. - */ - private final OptionalInt devServerPort; - - /** - * Command to start the dev sever. Will be "start" by default if not detected. - */ - private final String devServerCommand; - - /** - * 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 - */ - private final String buildDirectory; - - public QuinoaDirectoryBuildItem(PackageManager packageManager, String devServerCommand, OptionalInt devServerPort, - String buildDirectory) { - this.packageManager = packageManager; - this.devServerPort = devServerPort; - this.buildDirectory = buildDirectory; - this.devServerCommand = Objects.toString(devServerCommand, "start"); - } - - public PackageManager getPackageManager() { - return packageManager; - } - - public Path getDirectory() { - return getPackageManager().getDirectory(); - } - - public OptionalInt getDevServerPort() { - return devServerPort; - } - - public String getDevServerCommand() { - return devServerCommand; - } - - public String getBuildDirectory() { - return buildDirectory; - } - - public boolean isDevServerMode(DevServerConfig devServerConfig) { - return devServerConfig.enabled && devServerPort.isPresent(); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java index 17fed0c8..27fd4de3 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/QuinoaProcessor.java @@ -3,9 +3,12 @@ import static io.quarkiverse.quinoa.QuinoaRecorder.META_INF_WEB_UI; import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_ROUTE_ORDER; import static io.quarkiverse.quinoa.QuinoaRecorder.QUINOA_SPA_ROUTE_ORDER; -import static io.quarkiverse.quinoa.deployment.packagemanager.FrameworkType.detectFramework; -import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManager.autoDetectPackageManager; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isDevServerMode; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.isEnabled; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.toHandlerConfig; +import static io.quarkiverse.quinoa.deployment.framework.FrameworkType.overrideConfig; import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstall.install; +import static io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner.autoDetectPackageManager; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import java.io.IOException; @@ -13,13 +16,14 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.OptionalInt; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -27,11 +31,14 @@ import io.quarkiverse.quinoa.QuinoaHandlerConfig; import io.quarkiverse.quinoa.QuinoaRecorder; -import io.quarkiverse.quinoa.deployment.packagemanager.DetectedFramework; -import io.quarkiverse.quinoa.deployment.packagemanager.FrameworkType; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManager; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.framework.FrameworkType; +import io.quarkiverse.quinoa.deployment.items.BuiltResourcesBuildItem; +import io.quarkiverse.quinoa.deployment.items.ConfiguredQuinoaBuildItem; +import io.quarkiverse.quinoa.deployment.items.TargetDirBuildItem; import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstall; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; @@ -54,6 +61,11 @@ public class QuinoaProcessor { private static final Logger LOG = Logger.getLogger(QuinoaProcessor.class); + private static final Set IGNORE_WATCH = Set.of("node_modules", "target"); + private static final Set IGNORE_WATCH_BUILD_DIRS = Arrays.stream(FrameworkType.values()).sequential() + .map(frameworkType -> frameworkType.factory().getFrameworkBuildDir()) + .collect(Collectors.toSet()); + private static final Pattern IGNORE_WATCH_REGEX = Pattern.compile("^[.].+$"); // ignore "." directories private static final String FEATURE = "quinoa"; private static final String TARGET_DIR_NAME = "quinoa-build"; @@ -64,22 +76,22 @@ FeatureBuildItem feature() { } @BuildStep - public QuinoaDirectoryBuildItem prepareQuinoaDirectory( + public ConfiguredQuinoaBuildItem prepareQuinoaDirectory( LaunchModeBuildItem launchMode, LiveReloadBuildItem liveReload, - QuinoaConfig quinoaConfig, + QuinoaConfig userConfig, OutputTargetBuildItem outputTarget) { - if (!quinoaConfig.isEnabled()) { + if (!isEnabled(userConfig)) { LOG.info("Quinoa is disabled."); return null; } - if (launchMode.isTest() && quinoaConfig.enable.isEmpty()) { + if (launchMode.isTest() && userConfig.enabled().isEmpty()) { // Default to disabled in tests LOG.warn("Quinoa is disabled by default in tests."); return null; } - final String configuredDir = quinoaConfig.uiDir; - final ProjectDirs projectDirs = resolveProjectDirs(quinoaConfig, outputTarget); + final String configuredDir = userConfig.uiDir(); + final ProjectDirs projectDirs = resolveProjectDirs(userConfig, outputTarget); if (projectDirs == null) { return null; } @@ -87,61 +99,65 @@ public QuinoaDirectoryBuildItem prepareQuinoaDirectory( if (!Files.isRegularFile(packageJsonFile)) { throw new ConfigurationException("No package.json found in Web UI directory: '" + configuredDir + "'"); } - Optional packageManagerBinary = quinoaConfig.packageManager; + + final QuinoaConfig resolvedConfig = overrideConfig(launchMode, userConfig, packageJsonFile); + + Optional packageManagerBinary = resolvedConfig.packageManager(); List paths = new ArrayList<>(); - if (quinoaConfig.packageManagerInstall.enabled) { - final PackageManagerInstall.Installation result = install(quinoaConfig.packageManagerInstall, + if (resolvedConfig.packageManagerInstall().enabled()) { + final PackageManagerInstall.Installation result = install(resolvedConfig.packageManagerInstall(), projectDirs); packageManagerBinary = Optional.of(result.getPackageManagerBinary()); paths.add(result.getNodeDirPath()); } - final PackageManager packageManager = autoDetectPackageManager(packageManagerBinary, - quinoaConfig.packageManagerCommand, projectDirs.getUIDir(), paths); - final boolean alreadyInstalled = Files.isDirectory(packageManager.getDirectory().resolve("node_modules")); + final PackageManagerRunner packageManagerRunner = autoDetectPackageManager(packageManagerBinary, + resolvedConfig.packageManagerCommand(), projectDirs.getUIDir(), paths); + final boolean alreadyInstalled = Files.isDirectory(packageManagerRunner.getDirectory().resolve("node_modules")); final boolean packageFileModified = liveReload.isLiveReload() && liveReload.getChangedResources().stream().anyMatch(r -> r.equals(packageJsonFile.toString())); - if (quinoaConfig.forceInstall || !alreadyInstalled || packageFileModified) { - final boolean frozenLockfile = quinoaConfig.frozenLockfile.orElseGet(QuinoaProcessor::isCI); - packageManager.install(frozenLockfile); + if (resolvedConfig.forceInstall() || !alreadyInstalled || packageFileModified) { + final boolean ci = resolvedConfig.ci().orElseGet(QuinoaProcessor::isCI); + if (ci) { + packageManagerRunner.ci(); + } else { + packageManagerRunner.install(); + } + } // attempt to autoconfigure settings based on the framework being used - final DetectedFramework detectedFramework = detectFramework(launchMode, quinoaConfig, packageJsonFile); - - return initDefaultConfig(packageManager, launchMode, quinoaConfig, detectedFramework); + return new ConfiguredQuinoaBuildItem(packageManagerRunner, resolvedConfig); } @BuildStep public TargetDirBuildItem processBuild( - QuinoaConfig quinoaConfig, - Optional quinoaDir, + ConfiguredQuinoaBuildItem configuredQuinoa, OutputTargetBuildItem outputTarget, LaunchModeBuildItem launchMode, LiveReloadBuildItem liveReload) throws IOException { - if (quinoaDir.isEmpty()) { + if (configuredQuinoa == null) { return null; } - final QuinoaDirectoryBuildItem quinoaDirectoryBuildItem = quinoaDir.get(); - final PackageManager packageManager = quinoaDirectoryBuildItem.getPackageManager(); + final PackageManagerRunner packageManagerRunner = configuredQuinoa.getPackageManager(); final QuinoaLiveContext contextObject = liveReload.getContextObject(QuinoaLiveContext.class); if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT - && quinoaDirectoryBuildItem.isDevServerMode(quinoaConfig.devServer)) { + && isDevServerMode(configuredQuinoa.resolvedConfig())) { return null; } if (liveReload.isLiveReload() && liveReload.getChangedResources().stream() - .noneMatch(r -> r.startsWith(packageManager.getDirectory().toString())) + .noneMatch(r -> r.startsWith(packageManagerRunner.getDirectory().toString())) && contextObject != null) { return new TargetDirBuildItem(contextObject.getLocation()); } - if (quinoaConfig.runTests) { - packageManager.test(); + if (configuredQuinoa.resolvedConfig().runTests()) { + packageManagerRunner.test(); } - packageManager.build(launchMode.getLaunchMode()); - final String configuredBuildDir = quinoaDirectoryBuildItem.getBuildDirectory(); - final Path buildDir = packageManager.getDirectory().resolve(configuredBuildDir); + packageManagerRunner.build(launchMode.getLaunchMode()); + final String configuredBuildDir = configuredQuinoa.resolvedConfig().buildDir().orElseThrow(); + final Path buildDir = packageManagerRunner.getDirectory().resolve(configuredBuildDir); if (!Files.isDirectory(buildDir)) { throw new ConfigurationException("Quinoa build directory not found: '" + buildDir.toAbsolutePath() + "'", Set.of("quarkus.quinoa.build-dir")); @@ -179,10 +195,9 @@ public BuiltResourcesBuildItem prepareResourcesForOtherMode( @BuildStep void watchChanges( - QuinoaConfig quinoaConfig, - Optional quinoaDir, + Optional quinoaDir, BuildProducer watchedPaths) throws IOException { - if (quinoaDir.isEmpty() || quinoaDir.get().isDevServerMode(quinoaConfig.devServer)) { + if (quinoaDir.isEmpty() || isDevServerMode(quinoaDir.get().resolvedConfig())) { return; } scan(quinoaDir.get().getPackageManager().getDirectory(), watchedPaths); @@ -191,14 +206,14 @@ void watchChanges( @BuildStep @Record(RUNTIME_INIT) public void runtimeInit( - QuinoaConfig quinoaConfig, + ConfiguredQuinoaBuildItem configuredQuinoa, HttpBuildTimeConfig httpBuildTimeConfig, LaunchModeBuildItem launchMode, Optional uiResources, QuinoaRecorder recorder, BuildProducer routes, BuildProducer resumeOn404) throws IOException { - if (quinoaConfig.justBuild) { + if (configuredQuinoa != null && configuredQuinoa.resolvedConfig().justBuild()) { LOG.info("Quinoa is in build only mode"); return; } @@ -207,7 +222,7 @@ public void runtimeInit( if (uiResources.get().getDirectory().isPresent()) { directory = uiResources.get().getDirectory().get().toAbsolutePath().toString(); } - final QuinoaHandlerConfig handlerConfig = quinoaConfig.toHandlerConfig( + final QuinoaHandlerConfig handlerConfig = toHandlerConfig(configuredQuinoa.resolvedConfig(), !launchMode.getLaunchMode().isDevOrTest(), httpBuildTimeConfig); resumeOn404.produce(new ResumeOn404BuildItem()); @@ -215,7 +230,7 @@ public void runtimeInit( .handler(recorder.quinoaHandler(handlerConfig, directory, uiResources.get().getNames())) .build()); - if (quinoaConfig.enableSPARouting) { + if (configuredQuinoa.resolvedConfig().enableSPARouting()) { routes.produce(RouteBuildItem.builder().orderedRoute("/*", QUINOA_SPA_ROUTE_ORDER) .handler(recorder.quinoaSPARoutingHandler(handlerConfig)) .build()); @@ -224,48 +239,20 @@ public void runtimeInit( } @BuildStep(onlyIf = IsDevelopment.class) - List hotDeploymentWatchedFiles(QuinoaConfig quinoaConfig, + List hotDeploymentWatchedFiles(Optional configuredQuinoa, OutputTargetBuildItem outputTarget) { final List watchedFiles = new ArrayList<>(PackageManagerType.values().length); - final ProjectDirs projectDirs = resolveProjectDirs(quinoaConfig, outputTarget); - if (projectDirs == null) { - // UI dir is misconfigured so just skip watching files + if (configuredQuinoa.isEmpty()) { return watchedFiles; } + for (PackageManagerType pm : PackageManagerType.values()) { - final String watchFile = projectDirs.uiDir.resolve(pm.getLockFile()).toString(); + final String watchFile = configuredQuinoa.get().getDirectory().resolve(pm.getLockFile()).toString(); watchedFiles.add(new HotDeploymentWatchedFileBuildItem(watchFile)); } return watchedFiles; } - private QuinoaDirectoryBuildItem initDefaultConfig(PackageManager packageManager, LaunchModeBuildItem launchMode, - QuinoaConfig config, DetectedFramework detectedFramework) { - String buildDirectory = config.buildDir; - OptionalInt port = config.devServer.port; - - if (detectedFramework == null || detectedFramework.getFrameworkType() == null) { - // nothing to do as no framework was detected - return new QuinoaDirectoryBuildItem(packageManager, "start", port, buildDirectory); - } - - LOG.infof("%s", packageManager.getPackageManagerCommands()); - - // only override properties that have not been set - FrameworkType framework = detectedFramework.getFrameworkType(); - if (config.devServer.enabled && launchMode.getLaunchMode() != LaunchMode.NORMAL && port.isEmpty()) { - LOG.infof("%s framework setting dev server port: %d", framework, framework.getDevServerPort()); - port = OptionalInt.of(framework.getDevServerPort()); - LOG.infof("%s framework setting dev script: '%s'", framework, detectedFramework.getDevServerCommand()); - } - - if (QuinoaConfig.DEFAULT_BUILD_DIR.equalsIgnoreCase(buildDirectory)) { - buildDirectory = detectedFramework.getBuildDirectory(); - LOG.infof("%s framework setting build directory: '%s'", framework, buildDirectory); - } - return new QuinoaDirectoryBuildItem(packageManager, detectedFramework.getDevServerCommand(), port, buildDirectory); - } - private HashSet prepareBuiltResources( BuildProducer generatedResources, BuildProducer nativeImageResources, @@ -296,7 +283,7 @@ private void scan(Path directory, BuildProducer ignoreSet = new HashSet<>(); + ignoreSet.addAll(IGNORE_WATCH); + ignoreSet.addAll(IGNORE_WATCH_BUILD_DIRS); + final String directory = filePath.getFileName().toString(); + if (ignoreSet.contains(directory) || IGNORE_WATCH_REGEX.matcher(directory).matches()) { + return false; + } + return true; + } + private static ProjectDirs resolveProjectDirs(QuinoaConfig config, OutputTargetBuildItem outputTarget) { Path projectRoot = findProjectRoot(outputTarget.getOutputDirectory()); - Path configuredUIDirPath = Path.of(config.uiDir.trim()); + Path configuredUIDirPath = Path.of(config.uiDir().trim()); if (projectRoot == null || !Files.isDirectory(projectRoot)) { if (configuredUIDirPath.isAbsolute() && Files.isDirectory(configuredUIDirPath)) { return new ProjectDirs(null, configuredUIDirPath.normalize()); @@ -319,7 +330,7 @@ private static ProjectDirs resolveProjectDirs(QuinoaConfig config, if (!Files.isDirectory(uiRoot)) { LOG.warnf( "Quinoa directory not found 'quarkus.quinoa.ui-dir=%s' resolved to '%s'. It is recommended to remove the quarkus-quinoa extension if not used.", - config.uiDir, + config.uiDir(), uiRoot.toAbsolutePath()); return null; } diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java new file mode 100644 index 00000000..34fd30a9 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/DevServerConfig.java @@ -0,0 +1,75 @@ +package io.quarkiverse.quinoa.deployment.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithParentName; + +@ConfigGroup +public interface DevServerConfig { + + /** + * Enable external dev server (live coding). + * If the "dev-server.port" config is not detected or defined it will be disabled. + */ + @WithParentName + @WithDefault("true") + 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 + */ + @WithDefault("true") + 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. + * If the external server responds with a 404, it is ignored by Quinoa and processed like any other backend request. + */ + @ConfigDocDefault("framework detection or fallback to empty") + Optional port(); + + /** + * Host of the server to forward requests to. + */ + @WithDefault("localhost") + String host(); + + /** + * After start, Quinoa wait for the external dev server. + * by sending GET requests to this path waiting for a 200 status. + * If forced empty, Quinoa will not check if the dev server is up. + */ + @WithDefault("/") + Optional 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. + */ + @WithDefault("true") + boolean websocket(); + + /** + * Timeout in ms for the dev server to be up and running. + */ + @WithDefault("30000") + int checkTimeout(); + + /** + * Enable external dev server live coding logs. + * This is not enabled by default because most dev servers display compilation errors directly in the browser. + */ + @WithDefault("false") + boolean logs(); + + /** + * Set this value if the index page is different for the dev-server + */ + @ConfigDocDefault("auto-detected falling back to the quinoa.index-page") + Optional indexPage(); +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/FrameworkConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/FrameworkConfig.java new file mode 100644 index 00000000..32f7d1aa --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/FrameworkConfig.java @@ -0,0 +1,12 @@ +package io.quarkiverse.quinoa.deployment.config; + +import io.smallrye.config.WithDefault; + +public interface FrameworkConfig { + + /** + * When true, the UI Framework will be auto-detected if possible + */ + @WithDefault("true") + boolean detection(); +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerCommandConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerCommandConfig.java new file mode 100644 index 00000000..2c1cb8a2 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerCommandConfig.java @@ -0,0 +1,77 @@ +package io.quarkiverse.quinoa.deployment.config; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface PackageManagerCommandConfig { + + String DEFAULT_DEV_SCRIPT_NAME = "start"; + String DEFAULT_DEV_COMMAND = "run " + DEFAULT_DEV_SCRIPT_NAME; + String DEFAULT_INSTALL_COMMAND = "install"; + String DEFAULT_BUILD_COMMAND = "run build"; + String DEFAULT_TEST_COMMAND = "run test"; + + /** + * Custom command for installing all packages. + * e.g. «ci --cache $CACHE_DIR/.npm --prefer-offline» + */ + @ConfigDocDefault(DEFAULT_INSTALL_COMMAND) + Optional install(); + + /** + * Environment variables for install command execution. + */ + Map installEnv(); + + /** + * Custom command for installing all the packages without generating a lockfile (frozen lockfile) + * and failing if an update is needed (useful in CI). + */ + @ConfigDocDefault("Detected based on package manager") + Optional ci(); + + /** + * Environment variables for ci command execution. + */ + Map ciEnv(); + + /** + * Custom command for building the application. + */ + @WithDefault(DEFAULT_BUILD_COMMAND) + Optional build(); + + /** + * Environment variables for build command execution. + */ + Map buildEnv(); + + /** + * Custom command for running tests for the application. + */ + @ConfigDocDefault(DEFAULT_TEST_COMMAND) + Optional test(); + + /** + * Environment variables for test command execution. + */ + @ConfigDocDefault("CI=true") + Map testEnv(); + + /** + * Custom command for starting the application in development mode. + */ + @ConfigDocDefault("framework detection with fallback to '" + DEFAULT_DEV_SCRIPT_NAME + "'") + Optional dev(); + + /** + * Environment variables for development command execution. + */ + Map devEnv(); + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerInstallConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerInstallConfig.java new file mode 100644 index 00000000..b41ca5b2 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/PackageManagerInstallConfig.java @@ -0,0 +1,83 @@ +package io.quarkiverse.quinoa.deployment.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithParentName; + +@ConfigGroup +public interface PackageManagerInstallConfig { + + String NPM_PROVIDED = "provided"; + String DEFAULT_INSTALL_DIR = ".quinoa/"; + + /** + * Enable Package Manager Installation. + * This will override "package-manager" config. + * Set "quarkus.quinoa.package-manager-command.prepend-binary=true" + * when using with custom commands + */ + @WithParentName + @WithDefault("false") + boolean enabled(); + + /** + * The directory where NodeJS should be installed (relative to the project root), + * It will be installed in a 'node/' subdirectory of this. + */ + @WithDefault(DEFAULT_INSTALL_DIR) + String installDir(); + + /** + * The NodeJS Version to install locally to the project. + * Required when package-manager-install is enabled. + */ + Optional nodeVersion(); + + /** + * The NPM version to install and use. + * By default, the version is provided by NodeJS. + */ + @WithDefault(NPM_PROVIDED) + @ConfigDocDefault("'provided' means it will use the NPM embedded in NodeJS") + String npmVersion(); + + /** + * Where to download NPM from. + */ + @WithDefault("https://registry.npmjs.org/npm/-/") + String npmDownloadRoot(); + + /** + * Where to download NodeJS from. + */ + @WithDefault("https://nodejs.org/dist/") + String nodeDownloadRoot(); + + /** + * Install and use Yarn as package manager with this version. + * This is ignored if the npm-version is defined. + */ + Optional yarnVersion(); + + /** + * Where to download YARN from. + */ + @WithDefault("https://github.com/yarnpkg/yarn/releases/download/") + String yarnDownloadRoot(); + + /** + * Install and use PNPM as package manager with this version. + * This is ignored if the npm-version or the yarn-version is defined. + */ + Optional pnpmVersion(); + + /** + * Where to download PNPM from. + */ + @WithDefault("https://registry.npmjs.org/pnpm/-/") + String pnpmDownloadRoot(); + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java new file mode 100644 index 00000000..d4a29f01 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/QuinoaConfig.java @@ -0,0 +1,167 @@ +package io.quarkiverse.quinoa.deployment.config; + +import static java.util.stream.Collectors.toList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkiverse.quinoa.QuinoaHandlerConfig; +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; +import io.smallrye.config.WithParentName; + +@ConfigMapping(prefix = "quarkus.quinoa") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface QuinoaConfig { + + String DEFAULT_BUILD_DIR = "build/"; + String DEFAULT_WEB_UI_DIR = "src/main/webui"; + String DEFAULT_INDEX_PAGE = "index.html"; + + /** + * Indicate if the extension should be enabled. + */ + @WithParentName + @ConfigDocDefault("enabled (disabled in test mode)") + public Optional enabled(); + + /** + * 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). + */ + @WithDefault("false") + boolean justBuild(); + + /** + * Path to the Web UI (NodeJS) root directory (relative to the project root). + */ + @WithDefault(DEFAULT_WEB_UI_DIR) + String uiDir(); + + /** + * 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. + */ + @ConfigDocDefault("framework detection with fallback to '" + DEFAULT_BUILD_DIR + "'") + Optional buildDir(); + + /** + * Name of the package manager binary. + * Only npm, pnpm and yarn are supported for the moment. + */ + @ConfigDocDefault("auto-detected based on lockfile falling back to 'npm'") + Optional packageManager(); + + /** + * Configuration for installing the package manager + */ + PackageManagerInstallConfig packageManagerInstall(); + + /** + * Configuration for overriding build commands + */ + PackageManagerCommandConfig packageManagerCommand(); + + /** + * Name of the index page. + */ + @WithDefault(DEFAULT_INDEX_PAGE) + String indexPage(); + + /** + * Indicate if the Web UI should also be tested during the build phase (i.e: npm test). + * To be used in a {@link io.quarkus.test.junit.QuarkusTestProfile} to have Web UI test running during a + * {@link io.quarkus.test.junit.QuarkusTest} + */ + @SuppressWarnings("JavadocReference") + @WithDefault("false") + boolean runTests(); + + /** + * Install the packages without generating a lockfile (frozen lockfile) and failing if an update is needed (useful in CI). + */ + @ConfigDocDefault("true if environment CI=true") + Optional ci(); + + /** + * Force install packages before building. + * It will install packages only if the node_modules directory is absent or when the package.json is modified in dev-mode. + */ + @WithDefault("false") + boolean forceInstall(); + + /** + * Configure framework detection + */ + FrameworkConfig framework(); + + /** + * Enable SPA (Single Page Application) routing, all relevant requests will be re-routed to the index page. + * Currently, for technical reasons, the Quinoa SPA routing configuration won't work with RESTEasy Classic. + */ + @WithDefault("false") + @WithName("enable-spa-routing") + boolean enableSPARouting(); + + /** + * List of path prefixes to be ignored by Quinoa. + */ + @ConfigDocDefault("ignore values configured by 'quarkus.resteasy-reactive.path', 'quarkus.resteasy.path' and 'quarkus.http.non-application-root-path'") + Optional> ignoredPathPrefixes(); + + /** + * Configuration for the external dev server (live coding server) + */ + DevServerConfig devServer(); + + static List getNormalizedIgnoredPathPrefixes(QuinoaConfig config) { + return config.ignoredPathPrefixes().orElseGet(() -> { + Config allConfig = ConfigProvider.getConfig(); + List defaultIgnore = new ArrayList<>(); + readExternalConfigPath(allConfig, "quarkus.resteasy.path").ifPresent(defaultIgnore::add); + readExternalConfigPath(allConfig, "quarkus.resteasy-reactive.path").ifPresent(defaultIgnore::add); + readExternalConfigPath(allConfig, "quarkus.http.non-application-root-path").ifPresent(defaultIgnore::add); + return defaultIgnore; + }).stream().map(s -> s.startsWith("/") ? s : "/" + s).collect(toList()); + } + + static QuinoaHandlerConfig toHandlerConfig(QuinoaConfig config, boolean prodMode, + final HttpBuildTimeConfig httpBuildTimeConfig) { + final Set compressMediaTypes = httpBuildTimeConfig.compressMediaTypes.map(Set::copyOf).orElse(Set.of()); + final String indexPage = !isDevServerMode(config) ? config.indexPage() + : config.devServer().indexPage().orElse(config.indexPage()); + return new QuinoaHandlerConfig(getNormalizedIgnoredPathPrefixes(config), indexPage, prodMode, + httpBuildTimeConfig.enableCompression, compressMediaTypes); + } + + private static Optional readExternalConfigPath(Config config, String key) { + return config.getOptionalValue(key, String.class) + .filter(s -> !Objects.equals(s, "/")) + .map(s -> s.endsWith("/") ? s : s + "/"); + } + + static boolean isDevServerMode(QuinoaConfig config) { + return config.devServer().enabled() && config.devServer().port().isPresent(); + } + + static boolean isEnabled(QuinoaConfig config) { + return config.enabled().orElse(true); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/DevServerConfigDelegate.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/DevServerConfigDelegate.java new file mode 100644 index 00000000..28d71bb6 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/DevServerConfigDelegate.java @@ -0,0 +1,58 @@ +package io.quarkiverse.quinoa.deployment.config.delegate; + +import java.util.Optional; + +import io.quarkiverse.quinoa.deployment.config.DevServerConfig; + +public class DevServerConfigDelegate implements DevServerConfig { + private final DevServerConfig delegate; + + public DevServerConfigDelegate(DevServerConfig delegate) { + this.delegate = delegate; + } + + @Override + public boolean enabled() { + return delegate.enabled(); + } + + @Override + public boolean managed() { + return delegate.managed(); + } + + @Override + public Optional port() { + return delegate.port(); + } + + @Override + public String host() { + return delegate.host(); + } + + @Override + public Optional checkPath() { + return delegate.checkPath(); + } + + @Override + public boolean websocket() { + return delegate.websocket(); + } + + @Override + public int checkTimeout() { + return delegate.checkTimeout(); + } + + @Override + public boolean logs() { + return delegate.logs(); + } + + @Override + public Optional indexPage() { + return delegate.indexPage(); + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/PackageManagerCommandConfigDelegate.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/PackageManagerCommandConfigDelegate.java new file mode 100644 index 00000000..5a5a13aa --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/PackageManagerCommandConfigDelegate.java @@ -0,0 +1,65 @@ +package io.quarkiverse.quinoa.deployment.config.delegate; + +import java.util.Map; +import java.util.Optional; + +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; + +public class PackageManagerCommandConfigDelegate implements PackageManagerCommandConfig { + + private final PackageManagerCommandConfig delegate; + + public PackageManagerCommandConfigDelegate(PackageManagerCommandConfig delegate) { + this.delegate = delegate; + } + + @Override + public Optional ci() { + return delegate.ci(); + } + + @Override + public Map ciEnv() { + return delegate.ciEnv(); + } + + @Override + public Optional install() { + return delegate.install(); + } + + @Override + public Map installEnv() { + return delegate.installEnv(); + } + + @Override + public Optional build() { + return delegate.build(); + } + + @Override + public Map buildEnv() { + return delegate.buildEnv(); + } + + @Override + public Optional test() { + return delegate.test(); + } + + @Override + public Map testEnv() { + return delegate.testEnv(); + } + + @Override + public Optional dev() { + return delegate.dev(); + } + + @Override + public Map devEnv() { + return delegate.devEnv(); + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java new file mode 100644 index 00000000..f1cb76aa --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/config/delegate/QuinoaConfigDelegate.java @@ -0,0 +1,94 @@ +package io.quarkiverse.quinoa.deployment.config.delegate; + +import java.util.List; +import java.util.Optional; + +import io.quarkiverse.quinoa.deployment.config.DevServerConfig; +import io.quarkiverse.quinoa.deployment.config.FrameworkConfig; +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.config.PackageManagerInstallConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; + +public class QuinoaConfigDelegate implements QuinoaConfig { + private final QuinoaConfig delegate; + + public QuinoaConfigDelegate(QuinoaConfig delegate) { + this.delegate = delegate; + } + + @Override + public Optional enabled() { + return delegate.enabled(); + } + + @Override + public boolean justBuild() { + return delegate.justBuild(); + } + + @Override + public String uiDir() { + return delegate.uiDir(); + } + + @Override + public Optional buildDir() { + return delegate.buildDir(); + } + + @Override + public Optional packageManager() { + return delegate.packageManager(); + } + + @Override + public PackageManagerInstallConfig packageManagerInstall() { + return delegate.packageManagerInstall(); + } + + @Override + public PackageManagerCommandConfig packageManagerCommand() { + return delegate.packageManagerCommand(); + } + + @Override + public String indexPage() { + return delegate.indexPage(); + } + + @Override + public boolean runTests() { + return delegate.runTests(); + } + + @Override + public Optional ci() { + return delegate.ci(); + } + + @Override + public boolean forceInstall() { + return delegate.forceInstall(); + } + + @Override + public FrameworkConfig framework() { + return delegate.framework(); + } + + @Override + public boolean enableSPARouting() { + return delegate.enableSPARouting(); + } + + @Override + public Optional> ignoredPathPrefixes() { + return delegate.ignoredPathPrefixes(); + } + + @Override + public DevServerConfig devServer() { + return delegate.devServer(); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/devui/QuinoaDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/devui/QuinoaDevUIProcessor.java index 6934c43c..6b33e84a 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/devui/QuinoaDevUIProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/devui/QuinoaDevUIProcessor.java @@ -2,13 +2,12 @@ import java.util.Map; import java.util.Optional; -import java.util.OptionalInt; import java.util.function.Function; -import io.quarkiverse.quinoa.deployment.QuinoaConfig; -import io.quarkiverse.quinoa.deployment.QuinoaDirectoryBuildItem; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManager; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstallConfig; +import io.quarkiverse.quinoa.deployment.config.PackageManagerInstallConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.items.ConfiguredQuinoaBuildItem; +import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; import io.quarkiverse.quinoa.devui.QuinoaJsonRpcService; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; @@ -32,11 +31,11 @@ public class QuinoaDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) void createCard(BuildProducer cardPageBuildItemBuildProducer, BuildProducer footerProducer, - Optional quinoaDirectoryBuildItem, - QuinoaConfig quinoaConfig) { + Optional configuredQuinoa) { + QuinoaConfig quinoaConfig = configuredQuinoa.get().resolvedConfig(); final CardPageBuildItem card = new CardPageBuildItem(); - final Optional node = quinoaConfig.packageManagerInstall.nodeVersion; + final Optional node = quinoaConfig.packageManagerInstall().nodeVersion(); if (node.isPresent()) { final String nodeVersion = node.get(); final PageBuilder nodejsPage = Page.externalPageBuilder("Node.js") @@ -47,7 +46,7 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, card.addPage(nodejsPage); } - final String npmVersion = quinoaConfig.packageManagerInstall.npmVersion; + final String npmVersion = quinoaConfig.packageManagerInstall().npmVersion(); if (!PackageManagerInstallConfig.NPM_PROVIDED.equalsIgnoreCase(npmVersion)) { final PageBuilder nodejsPage = Page.externalPageBuilder("NPM") .icon("font-awesome-brands:square-js") @@ -56,7 +55,7 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, .staticLabel(npmVersion); card.addPage(nodejsPage); } else { - final Optional pnpmVersion = quinoaConfig.packageManagerInstall.pnpmVersion; + final Optional pnpmVersion = quinoaConfig.packageManagerInstall().pnpmVersion(); if (pnpmVersion.isPresent()) { final PageBuilder nodejsPage = Page.externalPageBuilder("PNPM") .icon("font-awesome-brands:square-js") @@ -65,7 +64,7 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, .staticLabel(pnpmVersion.get()); card.addPage(nodejsPage); } - final Optional yarnVersion = quinoaConfig.packageManagerInstall.yarnVersion; + final Optional yarnVersion = quinoaConfig.packageManagerInstall().yarnVersion(); if (yarnVersion.isPresent()) { final PageBuilder nodejsPage = Page.externalPageBuilder("Yarn") .icon("font-awesome-brands:square-js") @@ -76,14 +75,14 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, } } - if (quinoaDirectoryBuildItem.isPresent()) { - final OptionalInt port = quinoaDirectoryBuildItem.get().getDevServerPort(); - if (port.isPresent() && port.getAsInt() > 0) { + if (configuredQuinoa.isPresent()) { + final Optional port = configuredQuinoa.get().resolvedConfig().devServer().port(); + if (port.isPresent() && port.get() > 0) { final PageBuilder portPage = Page.externalPageBuilder("Port") .icon("font-awesome-solid:plug") - .url(String.format("https://localhost:%d", port.getAsInt())) + .url(String.format("https://localhost:%d", port.get())) .doNotEmbed() - .staticLabel(String.valueOf(port.getAsInt())); + .staticLabel(String.valueOf(port.get())); card.addPage(portPage); } } @@ -101,21 +100,21 @@ void createCard(BuildProducer cardPageBuildItemBuildProducer, } @BuildStep(onlyIf = IsDevelopment.class) - JsonRPCProvidersBuildItem registerJsonRpcBackend(Optional quinoaDirectoryBuildItem, + JsonRPCProvidersBuildItem registerJsonRpcBackend(Optional quinoaDirectoryBuildItem, QuinoaConfig quinoaConfig) { DevConsoleManager.register("quinoa-install-action", install(quinoaDirectoryBuildItem, quinoaConfig)); return new JsonRPCProvidersBuildItem(QuinoaJsonRpcService.class); } - private Function, String> install(Optional quinoaDirectoryBuildItem, + private Function, String> install(Optional quinoaDirectoryBuildItem, QuinoaConfig quinoaConfig) { return (map -> { try { - final PackageManager packageManager = quinoaDirectoryBuildItem.orElseThrow().getPackageManager(); + final PackageManagerRunner packageManagerRunner = quinoaDirectoryBuildItem.orElseThrow().getPackageManager(); // install or update packages - packageManager.install(false); + packageManagerRunner.install(); return "installed"; } catch (Exception e) { @@ -123,4 +122,4 @@ private Function, String> install(Optional packageJson); +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/FrameworkType.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/FrameworkType.java new file mode 100644 index 00000000..78e13a86 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/FrameworkType.java @@ -0,0 +1,142 @@ +package io.quarkiverse.quinoa.deployment.framework; + +import static io.quarkiverse.quinoa.deployment.framework.override.GenericFramework.UNKNOWN_FRAMEWORK; +import static io.quarkiverse.quinoa.deployment.framework.override.GenericFramework.generic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import jakarta.json.Json; +import jakarta.json.JsonException; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; + +import org.jboss.logging.Logger; + +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.framework.override.AngularFramework; +import io.quarkiverse.quinoa.deployment.framework.override.NextFramework; +import io.quarkiverse.quinoa.deployment.framework.override.ReactFramework; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; + +/** + * Configuration defaults for multiple JS frameworks that can be used to allow for easier adoption with less user configuration. + */ +public enum FrameworkType { + + REACT(Set.of("react-scripts", "react-app-rewired", "craco"), new ReactFramework()), + VUE_LEGACY(Set.of("vue-cli-service"), generic("dist", "serve", 3000)), + VITE(Set.of("vite"), generic("dist", "dev", 5173)), + SOLID_START(Set.of("solid-start"), generic("dist", "dev", 3000)), + ASTRO(Set.of("astro"), generic("dist", "dev", 3000)), + NEXT(Set.of("next"), new NextFramework()), + NUXT(Set.of("nuxt"), generic("dist", "dev", 3000)), + ANGULAR(Set.of("ng serve"), new AngularFramework()), + EMBER(Set.of("ember-cli"), generic("dist", "serve", 4200)), + AURELIA(Set.of("aurelia-cli"), generic("dist", "start", 8080)), + POLYMER(Set.of("polymer-cli"), generic("build", "serve", 8080)), + QWIK(Set.of("qwik"), generic("dist", "start", 5173)), + GATSBY(Set.of("gatsby-cli"), generic("dist", "develop", 8000)), + CYCLEJS(Set.of("cycle"), generic("build", "start", 8000)), + RIOTJS(Set.of("riot-cli"), generic("build", "start", 3000)), + MIDWAYJS(Set.of("midway"), generic("dist", "dev", 7001)), + REFINE(Set.of("refine"), generic("build", "dev", 3000)), + WEB_COMPONENTS(Set.of("web-dev-server"), generic("dist", "start", 8003)); + + private static final Logger LOG = Logger.getLogger(FrameworkType.class); + + public static final Set DEV_SCRIPTS = Arrays.stream(values()) + .map(framework -> framework.factory.getFrameworkDevScriptName()) + .collect(Collectors.toCollection(TreeSet::new)); + + private final Set cliStartDev; + private final FrameworkConfigOverrideFactory factory; + + FrameworkType(Set cliStartDev, FrameworkConfigOverrideFactory factory) { + this.cliStartDev = cliStartDev; + this.factory = factory; + } + + public FrameworkConfigOverrideFactory factory() { + return factory; + } + + public static QuinoaConfig overrideConfig(LaunchModeBuildItem launchMode, QuinoaConfig config, Path packageJsonFile) { + if (!config.framework().detection()) { + return UNKNOWN_FRAMEWORK.override(config, Optional.empty()); + } + + JsonObject packageJson = null; + try (JsonReader reader = Json.createReader(Files.newInputStream(packageJsonFile))) { + packageJson = reader.readObject(); + } catch (IOException | JsonException e) { + LOG.warnf("Quinoa failed to read the package.json file. %s", e.getMessage()); + } + + JsonString detectedDevScriptCommand = null; + String detectedDevScript = null; + + if (packageJson != null) { + JsonObject scripts = packageJson.getJsonObject("scripts"); + if (scripts != null) { + // loop over all possible start scripts until we find one + for (String devScript : FrameworkType.DEV_SCRIPTS) { + detectedDevScriptCommand = scripts.getJsonString(devScript); + if (detectedDevScriptCommand != null) { + detectedDevScript = devScript; + break; + } + } + } + } + + if (detectedDevScript == null) { + LOG.trace("Quinoa could not auto-detect the framework from package.json file."); + return UNKNOWN_FRAMEWORK.override(config, Optional.ofNullable(packageJson)); + } + + // check if we found a script to detect which framework + final FrameworkType frameworkType = resolveFramework(detectedDevScriptCommand.getString()); + if (frameworkType == null) { + LOG.info("Quinoa could not auto-detect the framework from package.json file."); + return UNKNOWN_FRAMEWORK.override(config, Optional.ofNullable(packageJson)); + } + + String expectedScript = frameworkType.factory.getFrameworkDevScriptName(); + if (launchMode.getLaunchMode().isDevOrTest() && !Objects.equals(detectedDevScript, expectedScript)) { + LOG.warnf("%s framework typically defines a '%s` script in package.json file but found '%s' instead.", + frameworkType, expectedScript, detectedDevScript); + } + + LOG.infof("Quinoa detected '%s' framework from package.json file.", frameworkType); + return frameworkType.factory.override(config, Optional.ofNullable(packageJson)); + } + + /** + * Try and detect the framework based on the script starting with a command like "vite" or "ng" + * + * @param script the script to check + * @return either NULL if no match or the matching framework if found + */ + private static FrameworkType resolveFramework(String script) { + final String lowerScript = script.toLowerCase(Locale.ROOT); + for (FrameworkType value : values()) { + for (String cliName : value.cliStartDev) { + if (lowerScript.contains(cliName)) { + return value; + } + } + } + return null; + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/AngularFramework.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/AngularFramework.java new file mode 100644 index 00000000..b1895dc8 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/AngularFramework.java @@ -0,0 +1,47 @@ +package io.quarkiverse.quinoa.deployment.framework.override; + +import java.util.Optional; + +import jakarta.json.JsonObject; + +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.config.delegate.PackageManagerCommandConfigDelegate; +import io.quarkiverse.quinoa.deployment.config.delegate.QuinoaConfigDelegate; + +public class AngularFramework extends GenericFramework { + + public AngularFramework() { + super("dist", "start", 4200); + } + + @Override + public QuinoaConfig override(QuinoaConfig delegate, Optional packageJson) { + return new QuinoaConfigDelegate(super.override(delegate, packageJson)) { + @Override + public Optional buildDir() { + // Angular builds a custom directory "dist/[appname]" + String applicationName = packageJson.map(p -> p.getString("name")).orElse("quinoa"); + final String fullBuildDir = String.format("%s/%s", getFrameworkBuildDir(), applicationName); + return Optional.of(delegate.buildDir().orElse(fullBuildDir)); + } + + @Override + public PackageManagerCommandConfig packageManagerCommand() { + return new PackageManagerCommandConfigDelegate(super.packageManagerCommand()) { + @Override + public Optional dev() { + return Optional.of(delegate.packageManagerCommand().dev() + .orElse("run " + getFrameworkDevScriptName() + " -- --disable-host-check")); + } + + @Override + public Optional test() { + return Optional.of(delegate.packageManagerCommand().test() + .orElse(DEFAULT_BUILD_COMMAND + " -- --no-watch --no-progress --browsers=ChromeHeadlessCI")); + } + }; + } + }; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/GenericFramework.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/GenericFramework.java new file mode 100644 index 00000000..d9bdf2a2 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/GenericFramework.java @@ -0,0 +1,85 @@ +package io.quarkiverse.quinoa.deployment.framework.override; + +import static io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig.DEFAULT_DEV_SCRIPT_NAME; +import static io.quarkiverse.quinoa.deployment.config.QuinoaConfig.DEFAULT_BUILD_DIR; + +import java.util.Optional; + +import jakarta.json.JsonObject; + +import io.quarkiverse.quinoa.deployment.config.DevServerConfig; +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.config.delegate.DevServerConfigDelegate; +import io.quarkiverse.quinoa.deployment.config.delegate.PackageManagerCommandConfigDelegate; +import io.quarkiverse.quinoa.deployment.config.delegate.QuinoaConfigDelegate; +import io.quarkiverse.quinoa.deployment.framework.FrameworkConfigOverrideFactory; + +public class GenericFramework implements FrameworkConfigOverrideFactory { + + public static FrameworkConfigOverrideFactory UNKNOWN_FRAMEWORK = new GenericFramework(DEFAULT_BUILD_DIR, + DEFAULT_DEV_SCRIPT_NAME); + private final String buildDir; + private final String scriptName; + private final Optional devServerPort; + + protected GenericFramework(String buildDir, String scriptName, Optional devServerPort) { + this.buildDir = buildDir; + this.scriptName = scriptName; + this.devServerPort = devServerPort; + } + + protected GenericFramework(String buildDir, String scriptName) { + this(buildDir, scriptName, Optional.empty()); + } + + protected GenericFramework(String buildDir, String scriptName, int devServerPort) { + this(buildDir, scriptName, Optional.of(devServerPort)); + } + + public static GenericFramework generic(String buildDir, String scriptName, int devServerPort) { + return new GenericFramework(buildDir, scriptName, devServerPort); + } + + @Override + public String getFrameworkBuildDir() { + return buildDir; + } + + @Override + public String getFrameworkDevScriptName() { + return scriptName; + } + + @Override + public QuinoaConfig override(QuinoaConfig delegate, Optional packageJson) { + return new QuinoaConfigDelegate(delegate) { + @Override + public Optional buildDir() { + return Optional.of(super.buildDir().orElse(buildDir)); + } + + @Override + public DevServerConfig devServer() { + return new DevServerConfigDelegate(super.devServer()) { + @Override + public Optional port() { + return Optional.ofNullable( + super.port().orElse( + devServerPort.orElse(null))); + } + }; + } + + @Override + public PackageManagerCommandConfig packageManagerCommand() { + return new PackageManagerCommandConfigDelegate(super.packageManagerCommand()) { + @Override + public Optional dev() { + return Optional.of(super.dev().orElse("run " + scriptName)); + } + }; + } + }; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/NextFramework.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/NextFramework.java new file mode 100644 index 00000000..412bb179 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/NextFramework.java @@ -0,0 +1,47 @@ +package io.quarkiverse.quinoa.deployment.framework.override; + +import java.util.Optional; + +import jakarta.json.JsonObject; + +import org.jboss.logging.Logger; + +import io.quarkiverse.quinoa.deployment.config.DevServerConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.config.delegate.DevServerConfigDelegate; +import io.quarkiverse.quinoa.deployment.config.delegate.QuinoaConfigDelegate; + +public class NextFramework extends GenericFramework { + + private static final Logger LOG = Logger.getLogger(NextFramework.class); + + public NextFramework() { + super("out", "dev", 3000); + } + + @Override + public QuinoaConfig override(QuinoaConfig delegate, Optional packageJson) { + if (delegate.packageManagerCommand().build().equals("run build") && packageJson.isPresent()) { + JsonObject scripts = packageJson.get().getJsonObject("scripts"); + if (scripts != null) { + if (!scripts.getString("build").contains("next export")) { + LOG.warn( + "Make sure you define \"build\": \"next build && next export\", in the package.json to make Next work with Quinoa."); + } + } + } + return new QuinoaConfigDelegate(super.override(delegate, packageJson)) { + + @Override + public DevServerConfig devServer() { + return new DevServerConfigDelegate(super.devServer()) { + @Override + public Optional indexPage() { + // In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html". + return Optional.of(super.indexPage().orElse("/")); + } + }; + } + }; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/ReactFramework.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/ReactFramework.java new file mode 100644 index 00000000..feecf3b1 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/framework/override/ReactFramework.java @@ -0,0 +1,38 @@ +package io.quarkiverse.quinoa.deployment.framework.override; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import jakarta.json.JsonObject; + +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.config.delegate.PackageManagerCommandConfigDelegate; +import io.quarkiverse.quinoa.deployment.config.delegate.QuinoaConfigDelegate; + +public class ReactFramework extends GenericFramework { + + public ReactFramework() { + super("build", "start", 3000); + } + + @Override + public QuinoaConfig override(QuinoaConfig delegate, Optional packageJson) { + return new QuinoaConfigDelegate(super.override(delegate, packageJson)) { + + @Override + public PackageManagerCommandConfig packageManagerCommand() { + return new PackageManagerCommandConfigDelegate(super.packageManagerCommand()) { + @Override + public Map devEnv() { + // BROWSER=NONE so the browser is not automatically opened with React + Map envs = new HashMap<>(super.testEnv()); + envs.put("BROWSER", "NONE"); + return envs; + } + }; + } + }; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/BuiltResourcesBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java similarity index 97% rename from deployment/src/main/java/io/quarkiverse/quinoa/deployment/BuiltResourcesBuildItem.java rename to deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java index de1610bf..53b4c9a0 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/BuiltResourcesBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/BuiltResourcesBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkiverse.quinoa.deployment; +package io.quarkiverse.quinoa.deployment.items; import java.nio.file.Path; import java.util.HashSet; diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ConfiguredQuinoaBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ConfiguredQuinoaBuildItem.java new file mode 100644 index 00000000..dfb5353e --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ConfiguredQuinoaBuildItem.java @@ -0,0 +1,32 @@ +package io.quarkiverse.quinoa.deployment.items; + +import java.nio.file.Path; + +import io.quarkiverse.quinoa.deployment.config.QuinoaConfig; +import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; +import io.quarkus.builder.item.SimpleBuildItem; + +public final class ConfiguredQuinoaBuildItem extends SimpleBuildItem { + + private final PackageManagerRunner packageManagerRunner; + + private final QuinoaConfig resolvedConfig; + + public ConfiguredQuinoaBuildItem(PackageManagerRunner packageManagerRunner, QuinoaConfig resolvedConfig) { + this.packageManagerRunner = packageManagerRunner; + this.resolvedConfig = resolvedConfig; + } + + public PackageManagerRunner getPackageManager() { + return packageManagerRunner; + } + + public Path getDirectory() { + return getPackageManager().getDirectory(); + } + + public QuinoaConfig resolvedConfig() { + return resolvedConfig; + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevServerBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ForwardedDevServerBuildItem.java similarity index 90% rename from deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevServerBuildItem.java rename to deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ForwardedDevServerBuildItem.java index 3a86d1ed..71ea8458 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/ForwardedDevServerBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/ForwardedDevServerBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkiverse.quinoa.deployment; +package io.quarkiverse.quinoa.deployment.items; import io.quarkus.builder.item.SimpleBuildItem; diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/TargetDirBuildItem.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/TargetDirBuildItem.java similarity index 88% rename from deployment/src/main/java/io/quarkiverse/quinoa/deployment/TargetDirBuildItem.java rename to deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/TargetDirBuildItem.java index 3c411866..2af9badb 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/TargetDirBuildItem.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/items/TargetDirBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkiverse.quinoa.deployment; +package io.quarkiverse.quinoa.deployment.items; import java.nio.file.Path; diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/Command.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/Command.java deleted file mode 100644 index c1541355..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/Command.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.util.Collections; -import java.util.Map; - -class Command { - public final Map envs; - public final String commandWithArguments; - - Command(String commandWithArguments) { - this.envs = Collections.emptyMap(); - this.commandWithArguments = commandWithArguments; - } - - Command(Map envs, String commandWithArguments) { - this.envs = envs; - this.commandWithArguments = commandWithArguments; - } -} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/DetectedFramework.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/DetectedFramework.java deleted file mode 100644 index 4ec250ab..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/DetectedFramework.java +++ /dev/null @@ -1,93 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.util.Objects; - -import jakarta.json.JsonObject; - -public class DetectedFramework { - - private FrameworkType frameworkType; - private JsonObject packageJson; - private String devServerCommand; - - public DetectedFramework() { - super(); - } - - public DetectedFramework(FrameworkType frameworkType, JsonObject packageJson, String devServerCommand) { - super(); - this.frameworkType = frameworkType; - this.packageJson = packageJson; - this.devServerCommand = devServerCommand; - } - - public FrameworkType getFrameworkType() { - return frameworkType; - } - - public void setFrameworkType(FrameworkType frameworkType) { - this.frameworkType = frameworkType; - } - - public JsonObject getPackageJson() { - return packageJson; - } - - public void setPackageJson(JsonObject packageJson) { - this.packageJson = packageJson; - } - - public String getDevServerCommand() { - return devServerCommand; - } - - public void setDevServerCommand(String devServerCommand) { - this.devServerCommand = devServerCommand; - } - - /** - * Gets the current framework build directory with special handling for Angular. - * - * @return the build directory for this framework type - */ - public String getBuildDirectory() { - FrameworkType framework = getFrameworkType(); - if (framework == null) { - return null; - } - - String buildDirectory = framework.getBuildDirectory(); - - // Angular builds a custom directory "dist/[appname]" - if (framework == FrameworkType.ANGULAR) { - String applicationName = Objects.toString(packageJson.getString("name"), "quinoa"); - buildDirectory = String.format(buildDirectory, applicationName); - } - return buildDirectory; - } - - @Override - public String toString() { - return "DetectedFramework{" + - "frameworkType=" + frameworkType + - ", packageJson=" + packageJson + - ", devServerCommand=" + devServerCommand + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - DetectedFramework that = (DetectedFramework) o; - return frameworkType == that.frameworkType && packageJson.equals(that.packageJson) - && devServerCommand.equals(that.devServerCommand); - } - - @Override - public int hashCode() { - return Objects.hash(frameworkType, packageJson, devServerCommand); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/EffectiveCommands.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/EffectiveCommands.java deleted file mode 100644 index 505768cc..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/EffectiveCommands.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.io.File; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import io.quarkus.runtime.LaunchMode; - -class EffectiveCommands implements PackageManagerCommands { - private static final String PATH_ENV_VAR = "PATH"; - private final PackageManagerCommands defaultCommands; - private final PackageManagerCommandConfig commandsConfig; - private final List paths; - - EffectiveCommands(PackageManagerCommands defaultCommands, PackageManagerCommandConfig commandsConfig, List paths) { - this.defaultCommands = defaultCommands; - this.commandsConfig = commandsConfig; - this.paths = paths; - } - - @Override - public Command install(boolean frozenLockfile) { - Command c = defaultCommands.install(frozenLockfile); - return new Command( - environment(c, commandsConfig.installEnv), - getCustomCommandWithArguments(commandsConfig.install) - .orElse(c.commandWithArguments)); - } - - @Override - public String binary() { - return defaultCommands.binary(); - } - - @Override - public Command build(LaunchMode mode) { - Command c = defaultCommands.build(mode); - return new Command( - environment(c, commandsConfig.buildEnv), - getCustomCommandWithArguments(commandsConfig.build) - .orElse(c.commandWithArguments)); - } - - @Override - public Command test() { - Command c = defaultCommands.test(); - return new Command( - environment(c, commandsConfig.testEnv), - getCustomCommandWithArguments(commandsConfig.test) - .orElse(c.commandWithArguments)); - } - - @Override - public Command dev(String command) { - Command c = defaultCommands.dev(command); - return new Command( - environment(c, commandsConfig.devEnv), - getCustomCommandWithArguments(commandsConfig.dev) - .orElse(c.commandWithArguments)); - } - - private Optional getCustomCommandWithArguments(Optional command) { - return command - .map(a -> commandsConfig.prependBinary ? defaultCommands.binary() + " " + a : a); - } - - private Map environment(final Command c, final Map additionalEnvironment) { - final Map environment = new HashMap<>(System.getenv()); - - environment.putAll(c.envs); - - if (additionalEnvironment != null) { - environment.putAll(additionalEnvironment); - } - - if (PackageManager.isWindows()) { - for (final Map.Entry entry : environment.entrySet()) { - final String pathName = entry.getKey(); - if (PATH_ENV_VAR.equalsIgnoreCase(pathName)) { - final String pathValue = entry.getValue(); - environment.put(pathName, extendPathVariable(pathValue, paths)); - } - } - } else { - final String pathValue = environment.get(PATH_ENV_VAR); - environment.put(PATH_ENV_VAR, extendPathVariable(pathValue, paths)); - } - - return environment; - } - - private String extendPathVariable(final String existingValue, final List paths) { - final StringBuilder pathBuilder = new StringBuilder(); - for (final String path : paths) { - pathBuilder.append(path).append(File.pathSeparator); - } - if (existingValue != null) { - pathBuilder.append(existingValue).append(File.pathSeparator); - } - return pathBuilder.toString(); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/FrameworkType.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/FrameworkType.java deleted file mode 100644 index 63c93955..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/FrameworkType.java +++ /dev/null @@ -1,192 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Locale; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; - -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonString; - -import org.jboss.logging.Logger; - -import io.quarkiverse.quinoa.deployment.QuinoaConfig; -import io.quarkus.deployment.builditem.LaunchModeBuildItem; - -/** - * Configuration defaults for multiple JS frameworks that can be used to allow for easier adoption with less user configuration. - */ -public enum FrameworkType { - - REACT("build", "start", 3000, Set.of("react-scripts", "react-app-rewired", "craco")), - VUE_LEGACY("dist", "serve", 3000, Set.of("vue-cli-service")), - VITE("dist", "dev", 5173, Set.of("vite")), - SOLID_START("dist", "dev", 3000, Set.of("solid-start")), - ASTRO("dist", "dev", 3000, Set.of("astro")), - NEXT("out", "dev", 3000, Set.of("next")), - NUXT("dist", "dev", 3000, Set.of("nuxt")), - ANGULAR("dist/%s", "start", 4200, Set.of("ng")), - EMBER("dist", "serve", 4200, Set.of("ember-cli")), - AURELIA("dist", "start", 8080, Set.of("aurelia-cli")), - POLYMER("build", "serve", 8080, Set.of("polymer-cli")), - QWIK("dist", "start", 5173, Set.of("qwik")), - GATSBY("dist", "develop", 8000, Set.of("gatsby-cli")), - CYCLEJS("build", "start", 8000, Set.of("cycle")), - RIOTJS("build", "start", 3000, Set.of("riot-cli")), - MIDWAYJS("dist", "dev", 7001, Set.of("midway")), - REFINE("build", "dev", 3000, Set.of("refine")), - WEB_COMPONENTS("dist", "start", 8003, Set.of("web-dev-server")); - - private static final Logger LOG = Logger.getLogger(FrameworkType.class); - - public static final Set DEV_SCRIPTS = Arrays.stream(values()).map(FrameworkType::getDevScript) - .collect(Collectors.toCollection(TreeSet::new)); - - private static final Set IGNORE_WATCH = Set.of("node_modules", "target"); - private static final String IGNORE_WATCH_REGEX = "^[.].+$"; // ignore "." directories - - /** - * 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. - */ - private final String buildDirectory; - - /** - * The script to run in package.json in dev mode typically "start" or "dev". - */ - private final String devScript; - - /** - * Default UI live-coding dev server port (proxy mode). - */ - private final int devServerPort; - - /** - * Match package.json scripts to detect this framework in use. - */ - private final Set packageScripts; - - FrameworkType(String buildDirectory, String devScript, int devServerPort, Set packageScripts) { - this.buildDirectory = buildDirectory; - this.devScript = devScript; - this.devServerPort = devServerPort; - this.packageScripts = packageScripts; - } - - public static DetectedFramework detectFramework(LaunchModeBuildItem launchMode, QuinoaConfig config, Path packageJsonFile) { - // only read package.json if the defaults are in use - if (config.devServer.port.isPresent() && !QuinoaConfig.DEFAULT_BUILD_DIR.equalsIgnoreCase(config.buildDir)) { - return new DetectedFramework(); - } - JsonObject packageJson = null; - JsonString startScript = null; - String startCommand = null; - try (JsonReader reader = Json.createReader(Files.newInputStream(packageJsonFile))) { - packageJson = reader.readObject(); - JsonObject scripts = packageJson.getJsonObject("scripts"); - if (scripts != null) { - // loop over all possible start scripts until we find one - for (String devScript : FrameworkType.DEV_SCRIPTS) { - startScript = scripts.getJsonString(devScript); - if (startScript != null) { - startCommand = devScript; - break; - } - } - } - } catch (IOException e) { - LOG.warnf("Quinoa failed to auto-detect the framework from package.json file. %s", e.getMessage()); - } - - if (startScript == null || startCommand == null) { - LOG.trace("Quinoa could not auto-detect the framework from package.json file."); - return new DetectedFramework(); - } - - // check if we found a script to detect which framework - final FrameworkType frameworkType = evaluate(startScript.getString()); - if (frameworkType == null) { - LOG.info("Quinoa could not auto-detect the framework from package.json file."); - return new DetectedFramework(); - } - - String expectedCommand = frameworkType.getDevScript(); - if (!Objects.equals(startCommand, expectedCommand)) { - LOG.warnf("%s framework typically defines a '%s` script in package.json file but found '%s' instead.", - frameworkType, expectedCommand, startCommand); - } - - LOG.infof("%s framework automatically detected from package.json file.", frameworkType); - return new DetectedFramework(frameworkType, packageJson, startCommand); - } - - /** - * Check whether this path should be scanned for changes by comparing against known directories that should be ignored. - * Ignored directories include any that start with DOT "." like ".next" or ".svelte", also "node_modules" and any - * of the framework build directories. - * - * @param filePath the file path to check - * @return true if it is a directory that should be scanned for changes, false if it should be ignored - */ - public static boolean shouldScanPath(Path filePath) { - if (!Files.isDirectory(filePath)) { - // not a directory so do not scan - return false; - } - - final Set ignoreSet = new HashSet<>(IGNORE_WATCH); - for (FrameworkType value : values()) { - String buildDirectory = value.getBuildDirectory(); - ignoreSet.add(buildDirectory); - } - final String directory = filePath.getFileName().toString(); - if (ignoreSet.contains(directory) || directory.matches(IGNORE_WATCH_REGEX)) { - return false; - } - - return true; - } - - /** - * Try and detect the framework based on the script starting with a command like "vite" or "ng" - * - * @param script the script to check - * @return either NULL if no match or the matching framework if found - */ - private static FrameworkType evaluate(String script) { - final String lowerScript = script.toLowerCase(Locale.ROOT); - for (FrameworkType value : values()) { - Set commands = value.getPackageScripts(); - for (String command : commands) { - if (lowerScript.startsWith(command)) { - return value; - } - } - } - return null; - } - - public String getBuildDirectory() { - return buildDirectory; - } - - public String getDevScript() { - return devScript; - } - - public Set getPackageScripts() { - return packageScripts; - } - - public int getDevServerPort() { - return devServerPort; - } -} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/NPMPackageManagerCommands.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/NPMPackageManagerCommands.java deleted file mode 100644 index e7f6f855..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/NPMPackageManagerCommands.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -class NPMPackageManagerCommands implements PackageManagerCommands { - private final String binary; - - public NPMPackageManagerCommands(String binary) { - this.binary = binary; - } - - @Override - public String binary() { - return binary; - } - - @Override - public Command install(boolean frozenLockfile) { - if (frozenLockfile) { - return new Command(binary() + " ci"); - } - return new Command(binary() + " install"); - } - -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PNPMPackageManagerCommands.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PNPMPackageManagerCommands.java deleted file mode 100644 index 8a5f60c7..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PNPMPackageManagerCommands.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -class PNPMPackageManagerCommands implements PackageManagerCommands { - - private final String binary; - - public PNPMPackageManagerCommands(String binary) { - this.binary = binary; - } - - @Override - public String binary() { - return binary; - } - - @Override - public Command install(boolean frozenLockfile) { - if (frozenLockfile) { - return new Command(binary() + " install --frozen-lockfile"); - } - return new Command(binary() + " install"); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommandConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommandConfig.java deleted file mode 100644 index 79d428f9..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommandConfig.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; -import io.smallrye.config.ConfigMapping; - -@ConfigGroup -public class PackageManagerCommandConfig { - - /** - * 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. - */ - @ConfigItem - public boolean prependBinary; - - /** - * Custom command for installing all dependencies. - * e.g. «npm ci --cache $CACHE_DIR/.npm --prefer-offline» - */ - @ConfigItem - public Optional install; - - /** - * Environment variables for install command execution. - */ - @ConfigMapping - public Map installEnv; - - /** - * Custom command for building the application. - */ - @ConfigItem - public Optional build; - - /** - * Environment variables for build command execution. - */ - @ConfigMapping - public Map buildEnv; - - /** - * Custom command for running tests for the application. - */ - @ConfigItem - public Optional test; - - /** - * Environment variables for test command execution. - */ - @ConfigMapping - public Map testEnv; - - /** - * Custom command for starting the application in development mode. - */ - @ConfigItem - public Optional dev; - - /** - * Environment variables for development command execution. - */ - @ConfigMapping - public Map devEnv; - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - PackageManagerCommandConfig that = (PackageManagerCommandConfig) o; - return Objects.equals(install, that.install) && Objects.equals(installEnv, that.installEnv) - && Objects.equals(build, that.build) && Objects.equals(buildEnv, that.buildEnv) - && Objects.equals(test, that.test) && Objects.equals(testEnv, that.testEnv) && Objects.equals(dev, that.dev) - && Objects.equals(devEnv, that.devEnv); - } - - @Override - public int hashCode() { - return Objects.hash(install, installEnv, build, buildEnv, test, testEnv, dev, devEnv); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommands.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommands.java deleted file mode 100644 index 7851a6fd..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerCommands.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.util.Collections; -import java.util.Map; - -import io.quarkus.runtime.LaunchMode; - -interface PackageManagerCommands { - Command install(boolean frozenLockfile); - - String binary(); - - default Command build(LaunchMode mode) { - // MODE=dev/test/prod to be able to build differently depending on the mode - // NODE_ENV=development/production - final Map env = Map.of( - "MODE", mode.getDefaultProfile(), - "NODE_ENV", mode.equals(LaunchMode.DEVELOPMENT) ? "development" : "production"); - return new Command(env, binary() + " run build"); - } - - default Command test() { - // CI=true to avoid watch mode on Angular - return new Command(Collections.singletonMap("CI", "true"), binary() + " test"); - } - - default Command dev(String command) { - // BROWSER=NONE so the browser is not automatically opened with React - return new Command(Collections.singletonMap("BROWSER", "none"), binary() + " run " + command); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstall.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstall.java index a7eacc18..6e9035f5 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstall.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstall.java @@ -13,13 +13,14 @@ import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig; import io.quarkiverse.quinoa.deployment.QuinoaProcessor.ProjectDirs; +import io.quarkiverse.quinoa.deployment.config.PackageManagerInstallConfig; import io.quarkus.runtime.configuration.ConfigurationException; public final class PackageManagerInstall { private static final Logger LOG = Logger.getLogger(PackageManagerInstall.class); private static final String INSTALL_SUB_PATH = "node"; - public static final String NODE_BINARY = PackageManager.isWindows() ? "node.exe" : "node"; + public static final String NODE_BINARY = PackageManagerRunner.isWindows() ? "node.exe" : "node"; public static final String NPM_PATH = INSTALL_SUB_PATH + "/node_modules/npm/bin/npm-cli.js"; public static final String PNPM_PATH = INSTALL_SUB_PATH + "/node_modules/corepack/dist/pnpm.js"; public static final String YARN_PATH = INSTALL_SUB_PATH + "/node_modules/corepack/dist/yarn.js"; @@ -31,11 +32,11 @@ private PackageManagerInstall() { public static Installation install(PackageManagerInstallConfig config, final ProjectDirs projectDirs) { Path installDir = resolveInstallDir(config, projectDirs).normalize(); FrontendPluginFactory factory = new FrontendPluginFactory(null, installDir.toFile()); - if (config.nodeVersion.isEmpty()) { + if (config.nodeVersion().isEmpty()) { throw new ConfigurationException("node-version is required to install package manager", Set.of("quarkus.quinoa.package-manager-install.node-version")); } - if (Integer.parseInt(config.nodeVersion.get().split("[.]")[0]) < 4) { + if (Integer.parseInt(config.nodeVersion().get().split("[.]")[0]) < 4) { throw new ConfigurationException("Quinoa is not compatible with Node prior to v4.0.0", Set.of("quarkus.quinoa.package-manager-install.node-version")); } @@ -43,9 +44,9 @@ public static Installation install(PackageManagerInstallConfig config, final Pro final ProxyConfig proxy = new ProxyConfig(Collections.emptyList()); try { factory.getNodeInstaller(proxy) - .setNodeVersion("v" + config.nodeVersion.get()) - .setNodeDownloadRoot(config.nodeDownloadRoot) - .setNpmVersion(config.npmVersion) + .setNodeVersion("v" + config.nodeVersion().get()) + .setNodeDownloadRoot(config.nodeDownloadRoot()) + .setNpmVersion(config.npmVersion()) .install(); } catch (InstallationException e) { if (e.getCause() instanceof DirectoryNotEmptyException && e.getCause().getMessage().contains("tmp")) { @@ -58,35 +59,35 @@ public static Installation install(PackageManagerInstallConfig config, final Pro // Use npm if npmVersion is different from provided or if no other version is set (then it will use the version provided by nodejs) String executionPath = NPM_PATH; - final String npmVersion = config.npmVersion; + final String npmVersion = config.npmVersion(); boolean isNpmProvided = PackageManagerInstallConfig.NPM_PROVIDED.equalsIgnoreCase(npmVersion); if (!isNpmProvided) { factory.getNPMInstaller(proxy) - .setNodeVersion("v" + config.nodeVersion.get()) + .setNodeVersion("v" + config.nodeVersion().get()) .setNpmVersion(npmVersion) - .setNpmDownloadRoot(config.npmDownloadRoot) + .setNpmDownloadRoot(config.npmDownloadRoot()) .install(); } // Use yarn if yarnVersion is set (and npm is provided) - final Optional yarnVersion = config.yarnVersion; + final Optional yarnVersion = config.yarnVersion(); if (yarnVersion.isPresent() && isNpmProvided) { executionPath = YARN_PATH; factory.getYarnInstaller(proxy) - .setYarnVersion("v" + config.yarnVersion.get()) - .setYarnDownloadRoot(config.yarnDownloadRoot) + .setYarnVersion("v" + config.yarnVersion().get()) + .setYarnDownloadRoot(config.yarnDownloadRoot()) .setIsYarnBerry(true) .install(); } // Use pnpm if pnpmVersion is set (and npm is provided and yarnVersion is not set) - final Optional pnpmVersion = config.pnpmVersion; + final Optional pnpmVersion = config.pnpmVersion(); if (pnpmVersion.isPresent() && isNpmProvided && yarnVersion.isEmpty()) { executionPath = PNPM_PATH; factory.getPnpmInstaller(proxy) - .setNodeVersion("v" + config.nodeVersion.get()) + .setNodeVersion("v" + config.nodeVersion().get()) .setPnpmVersion(pnpmVersion.get()) - .setPnpmDownloadRoot(config.pnpmDownloadRoot) + .setPnpmDownloadRoot(config.pnpmDownloadRoot()) .install(); } @@ -97,7 +98,7 @@ public static Installation install(PackageManagerInstallConfig config, final Pro } private static Path resolveInstallDir(PackageManagerInstallConfig config, ProjectDirs projectDirs) { - final Path installPath = Path.of(config.installDir.trim()); + final Path installPath = Path.of(config.installDir().trim()); if (installPath.isAbsolute()) { return installPath; } @@ -120,7 +121,7 @@ private static Installation resolveInstalledExecutorBinary(Path installDirectory } public static String normalizePath(String path) { - return PackageManager.isWindows() ? path.replaceAll("/", "\\\\") : path; + return PackageManagerRunner.isWindows() ? path.replaceAll("/", "\\\\") : path; } public static String quotePathWithSpaces(String path) { diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstallConfig.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstallConfig.java deleted file mode 100644 index ddeb6059..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerInstallConfig.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -import java.util.Objects; -import java.util.Optional; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup -public class PackageManagerInstallConfig { - - public static final String NPM_PROVIDED = "provided"; - private static final String DEFAULT_INSTALL_DIR = ".quinoa/"; - - /** - * Enable Package Manager Installation. - * This will override "package-manager" config. - * Set "quarkus.quinoa.package-manager-command.prepend-binary=true" - * when using with custom commands - */ - @ConfigItem(name = ConfigItem.PARENT, defaultValue = "false") - public boolean enabled; - - /** - * The directory where NodeJS should be installed, - * it will be installed in a node/ sub-directory. - * Default is ${project.root}/.quinoa - */ - @ConfigItem(defaultValue = DEFAULT_INSTALL_DIR) - public String installDir; - - /** - * The NodeJS Version to install locally to the project. - * Required when package-manager-install is enabled. - */ - @ConfigItem - public Optional nodeVersion; - - /** - * Where to download NodeJS from. - */ - @ConfigItem(defaultValue = "https://nodejs.org/dist/") - public String nodeDownloadRoot; - - /** - * The NPM version to install. - * By default, the version is provided by NodeJS. - */ - @ConfigItem(defaultValue = NPM_PROVIDED) - public String npmVersion; - - /** - * Where to download NPM from. - */ - @ConfigItem(defaultValue = "https://registry.npmjs.org/npm/-/") - public String npmDownloadRoot; - - /** - * 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. - */ - @ConfigItem - public Optional pnpmVersion; - - /** - * Where to download PNPM from. - */ - @ConfigItem(defaultValue = "https://registry.npmjs.org/pnpm/-/") - public String pnpmDownloadRoot; - - /** - * The YARN version to install. - * If the version is set and NPM Version is not set, then this version will attempt to be downloaded. - */ - @ConfigItem - public Optional yarnVersion; - - /** - * Where to download YARN from. - */ - @ConfigItem(defaultValue = "https://github.com/yarnpkg/yarn/releases/download/") - public String yarnDownloadRoot; - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - PackageManagerInstallConfig that = (PackageManagerInstallConfig) o; - return enabled == that.enabled && Objects.equals(nodeVersion, that.nodeVersion) - && Objects.equals(nodeDownloadRoot, that.nodeDownloadRoot) && Objects.equals(npmVersion, that.npmVersion) - && Objects.equals(npmDownloadRoot, that.npmDownloadRoot) && Objects.equals(pnpmVersion, that.pnpmVersion) - && Objects.equals(pnpmDownloadRoot, that.pnpmDownloadRoot) && Objects.equals(yarnVersion, that.yarnVersion) - && Objects.equals(yarnDownloadRoot, that.yarnDownloadRoot); - } - - @Override - public int hashCode() { - return Objects.hash(enabled, nodeVersion, nodeDownloadRoot, npmVersion, npmDownloadRoot, pnpmVersion, pnpmDownloadRoot, - yarnVersion, yarnDownloadRoot); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManager.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerRunner.java similarity index 73% rename from deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManager.java rename to deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerRunner.java index 71c27f22..d3b918f9 100644 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManager.java +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerRunner.java @@ -1,5 +1,6 @@ package io.quarkiverse.quinoa.deployment.packagemanager; +import static io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType.PNPM; import static java.lang.String.format; import java.io.BufferedReader; @@ -18,35 +19,53 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import org.jboss.logging.Logger; +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManager; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.deployment.util.ProcessUtil; import io.quarkus.dev.console.QuarkusConsole; import io.quarkus.runtime.LaunchMode; -public class PackageManager { - private static final Logger LOG = Logger.getLogger(PackageManager.class); +public class PackageManagerRunner { + private static final Logger LOG = Logger.getLogger(PackageManagerRunner.class); + public static final Predicate DEV_PROCESS_THREAD_PREDICATE = thread -> thread.getName() + .matches("Process (stdout|stderr) streamer"); private final Path directory; - private final PackageManagerCommands packageManagerCommands; + private final PackageManager packageManager; - private PackageManager(Path directory, PackageManagerCommands packageManagerCommands) { + private PackageManagerRunner(Path directory, PackageManager packageManager) { this.directory = directory; - this.packageManagerCommands = packageManagerCommands; + this.packageManager = packageManager; } public Path getDirectory() { return directory; } - public PackageManagerCommands getPackageManagerCommands() { - return packageManagerCommands; + public PackageManager getPackageManager() { + return packageManager; } - public void install(boolean frozenLockfile) { - final Command install = packageManagerCommands.install(frozenLockfile); + public void ci() { + final PackageManager.Command ci = packageManager.ci(); + LOG.infof("Running Quinoa package manager ci command: %s", ci.commandWithArguments); + if (!exec(ci)) { + throw new RuntimeException( + format("Error in Quinoa while running package manager ci command: %s", ci.commandWithArguments)); + } + } + + public void install() { + final PackageManager.Command install = packageManager.install(); LOG.infof("Running Quinoa package manager install command: %s", install.commandWithArguments); if (!exec(install)) { throw new RuntimeException( @@ -55,7 +74,7 @@ public void install(boolean frozenLockfile) { } public void build(LaunchMode mode) { - final Command build = packageManagerCommands.build(mode); + final PackageManager.Command build = packageManager.build(mode); LOG.infof("Running Quinoa package manager build command: %s", build.commandWithArguments); if (!exec(build)) { throw new RuntimeException( @@ -64,7 +83,7 @@ public void build(LaunchMode mode) { } public void test() { - final Command test = packageManagerCommands.test(); + final PackageManager.Command test = packageManager.test(); LOG.infof("Running Quinoa package manager test command: %s", test.commandWithArguments); if (!exec(test)) { throw new RuntimeException( @@ -114,9 +133,15 @@ private static void killDescendants(ProcessHandle process, boolean force) { }); } - public DevServer dev(String devServerCommand, String devServerHost, int devServerPort, String checkPath, int checkTimeout) { - final Command dev = packageManagerCommands.dev(devServerCommand); + public DevServer dev(Optional consoleInstalled, LoggingSetupBuildItem loggingSetup, + String devServerHost, int devServerPort, String checkPath, int checkTimeout) { + final PackageManager.Command dev = packageManager.dev(); LOG.infof("Running Quinoa package manager live coding as a dev service: %s", dev.commandWithArguments); + StartupLogCompressor logCompressor = new StartupLogCompressor( + "Quinoa package manager live coding dev service starting:", + consoleInstalled, + loggingSetup, + DEV_PROCESS_THREAD_PREDICATE); Process p = process(dev); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { @@ -125,7 +150,7 @@ public void run() { }); if (checkPath == null) { LOG.infof("Quinoa is configured to continue without check if the live coding server is up"); - return new DevServer(p, devServerHost); + return new DevServer(p, devServerHost, logCompressor); } String ipAddress = null; try { @@ -146,48 +171,34 @@ public void run() { Thread.currentThread().interrupt(); throw new RuntimeException(e); } - return new DevServer(p, ipAddress); + return new DevServer(p, ipAddress, logCompressor); } - public static PackageManager autoDetectPackageManager(Optional binary, + public static PackageManagerRunner autoDetectPackageManager(Optional binary, PackageManagerCommandConfig packageManagerCommands, Path directory, List paths) { - String resolved = null; + String resolvedBinary = null; if (binary.isEmpty()) { if (Files.isRegularFile(directory.resolve(PackageManagerType.YARN.getLockFile()))) { - resolved = PackageManagerType.YARN.getCommand(); - } else if (Files.isRegularFile(directory.resolve(PackageManagerType.PNPM.getLockFile()))) { - resolved = PackageManagerType.PNPM.getCommand(); + resolvedBinary = PackageManagerType.YARN.getBinary(); + } else if (Files.isRegularFile(directory.resolve(PNPM.getLockFile()))) { + resolvedBinary = PNPM.getBinary(); } else { - resolved = PackageManagerType.NPM.getCommand(); + resolvedBinary = PackageManagerType.NPM.getBinary(); } if (isWindows()) { - resolved = resolved + ".cmd"; + resolvedBinary = resolvedBinary + ".cmd"; } } else { - resolved = binary.get(); + resolvedBinary = binary.get(); } - return new PackageManager(directory, resolveCommands(resolved, packageManagerCommands, paths)); + return new PackageManagerRunner(directory, PackageManager.resolve(resolvedBinary, packageManagerCommands, paths)); } public static boolean isWindows() { return QuarkusConsole.IS_WINDOWS; } - static PackageManagerCommands resolveCommands(String binary, PackageManagerCommandConfig packageManagerCommands, - List paths) { - if (binary.contains(PackageManagerType.PNPM.getCommand())) { - return new EffectiveCommands(new PNPMPackageManagerCommands(binary), packageManagerCommands, paths); - } - if (binary.contains(PackageManagerType.NPM.getCommand())) { - return new EffectiveCommands(new NPMPackageManagerCommands(binary), packageManagerCommands, paths); - } - if (binary.contains(PackageManagerType.YARN.getCommand())) { - return new EffectiveCommands(new YarnPackageManagerCommands(binary), packageManagerCommands, paths); - } - throw new UnsupportedOperationException("Unsupported package manager binary: " + binary); - } - - private Process process(Command command) { + private Process process(PackageManager.Command command) { Process process = null; final ProcessBuilder builder = new ProcessBuilder() .directory(directory.toFile()) @@ -203,7 +214,7 @@ private Process process(Command command) { return process; } - private boolean exec(Command command) { + private boolean exec(PackageManager.Command command) { Process process = null; try { final ProcessBuilder processBuilder = new ProcessBuilder(); @@ -225,7 +236,7 @@ private boolean exec(Command command) { return process != null && process.exitValue() == 0; } - private String[] runner(Command command) { + private String[] runner(PackageManager.Command command) { if (isWindows()) { return new String[] { "cmd.exe", "/c", command.commandWithArguments }; } else { @@ -266,6 +277,9 @@ public void run() { } public static String isDevServerUp(String host, int port, String path) { + if (path == null) { + return host; + } final String normalizedPath = path.indexOf("/") == 0 ? path : "/" + path; try { InetAddress[] addresses = InetAddress.getAllByName(host); @@ -297,9 +311,12 @@ public static class DevServer { private final Process process; private final String hostAddress; - public DevServer(Process process, String hostAddress) { + private final StartupLogCompressor logCompressor; + + public DevServer(Process process, String hostAddress, StartupLogCompressor logCompressor) { this.process = process; this.hostAddress = hostAddress; + this.logCompressor = logCompressor; } public Process process() { @@ -309,5 +326,9 @@ public Process process() { public String hostAddress() { return hostAddress; } + + public StartupLogCompressor logCompressor() { + return logCompressor; + } } } diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerType.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerType.java deleted file mode 100644 index 9057a64f..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/PackageManagerType.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -public enum PackageManagerType { - - YARN("yarn", "yarn.lock"), - NPM("npm", "package-lock.json"), - PNPM("pnpm", "pnpm-lock.yaml"); - - private final String command; - private final String lockFile; - - PackageManagerType(String command, String lockFile) { - this.command = command; - this.lockFile = lockFile; - } - - public String getCommand() { - return command; - } - - public String getLockFile() { - return lockFile; - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/YarnPackageManagerCommands.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/YarnPackageManagerCommands.java deleted file mode 100644 index f88921b5..00000000 --- a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/YarnPackageManagerCommands.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkiverse.quinoa.deployment.packagemanager; - -class YarnPackageManagerCommands implements PackageManagerCommands { - private final String binary; - - public YarnPackageManagerCommands(String binary) { - this.binary = binary; - } - - @Override - public String binary() { - return binary; - } - - @Override - public Command install(boolean frozenLockfile) { - if (frozenLockfile) { - return new Command(binary() + " install --frozen-lockfile"); - } - return new Command(binary() + " install"); - } -} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/ConfiguredPackageManager.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/ConfiguredPackageManager.java new file mode 100644 index 00000000..ee384d43 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/ConfiguredPackageManager.java @@ -0,0 +1,123 @@ +package io.quarkiverse.quinoa.deployment.packagemanager.types; + +import static io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig.DEFAULT_BUILD_COMMAND; +import static io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig.DEFAULT_INSTALL_COMMAND; +import static io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig.DEFAULT_TEST_COMMAND; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner; +import io.quarkus.runtime.LaunchMode; + +class ConfiguredPackageManager implements PackageManager { + private static final String PATH_ENV_VAR = "PATH"; + + private final PackageManagerType type; + private final String binary; + private final PackageManagerCommandConfig commandsConfig; + private final List paths; + + ConfiguredPackageManager(PackageManagerType type, String binary, PackageManagerCommandConfig commandsConfig, + List paths) { + this.type = type; + this.binary = binary; + this.commandsConfig = commandsConfig; + this.paths = paths; + } + + @Override + public Command ci() { + return new Command( + environment(Map.of(), commandsConfig.ciEnv()), + prepareCommandWithArguments(commandsConfig.ci().orElse(type.ciCommand()))); + } + + @Override + public Command install() { + return new Command( + environment(Map.of(), commandsConfig.installEnv()), + prepareCommandWithArguments(commandsConfig.install().orElse(DEFAULT_INSTALL_COMMAND))); + } + + @Override + public String binary() { + return binary; + } + + @Override + public Command build(LaunchMode mode) { + // MODE=dev/test/prod to be able to build differently depending on the mode + // NODE_ENV=development/production + final Map env = Map.of( + "MODE", mode.getDefaultProfile(), + "NODE_ENV", mode.equals(LaunchMode.DEVELOPMENT) ? "development" : "production"); + return new Command( + environment(env, commandsConfig.buildEnv()), + prepareCommandWithArguments(commandsConfig.build().orElse(DEFAULT_BUILD_COMMAND))); + } + + @Override + public Command test() { + final Map testEnv = commandsConfig.testEnv(); + if (testEnv.isEmpty()) { + testEnv.put("CI", "true"); + } + return new Command( + environment(Map.of(), testEnv), + prepareCommandWithArguments(commandsConfig.test().orElse(DEFAULT_TEST_COMMAND))); + } + + @Override + public Command dev() { + return new Command( + environment(Map.of(), commandsConfig.devEnv()), + prepareCommandWithArguments(commandsConfig.dev().orElseThrow())); + } + + private String prepareCommandWithArguments(String command) { + return String.format("%s %s", binary(), command); + } + + private Map environment(final Map configuredEnvs, + final Map additionalEnvironment) { + final Map environment = new HashMap<>(System.getenv()); + + if (configuredEnvs != null) { + environment.putAll(configuredEnvs); + } + + if (additionalEnvironment != null) { + environment.putAll(additionalEnvironment); + } + + if (PackageManagerRunner.isWindows()) { + for (final Map.Entry entry : environment.entrySet()) { + final String pathName = entry.getKey(); + if (PATH_ENV_VAR.equalsIgnoreCase(pathName)) { + final String pathValue = entry.getValue(); + environment.put(pathName, extendPathVariable(pathValue, paths)); + } + } + } else { + final String pathValue = environment.get(PATH_ENV_VAR); + environment.put(PATH_ENV_VAR, extendPathVariable(pathValue, paths)); + } + + return environment; + } + + private String extendPathVariable(final String existingValue, final List paths) { + final StringBuilder pathBuilder = new StringBuilder(); + for (final String path : paths) { + pathBuilder.append(path).append(File.pathSeparator); + } + if (existingValue != null) { + pathBuilder.append(existingValue).append(File.pathSeparator); + } + return pathBuilder.toString(); + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManager.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManager.java new file mode 100644 index 00000000..15ef588b --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManager.java @@ -0,0 +1,50 @@ +package io.quarkiverse.quinoa.deployment.packagemanager.types; + +import static io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType.resolveType; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.quarkiverse.quinoa.deployment.config.PackageManagerCommandConfig; +import io.quarkus.runtime.LaunchMode; + +public interface PackageManager { + + Command ci(); + + Command install(); + + String binary(); + + Command build(LaunchMode mode); + + Command test(); + + Command dev(); + + public static PackageManager resolve(String binary, PackageManagerCommandConfig packageManagerCommands, + List paths) { + return configure(resolveType(binary), binary, packageManagerCommands, paths); + } + + private static PackageManager configure(PackageManagerType type, String binary, PackageManagerCommandConfig commandsConfig, + List paths) { + return new ConfiguredPackageManager(type, binary, commandsConfig, paths); + } + + class Command { + public final Map envs; + public final String commandWithArguments; + + Command(String commandWithArguments) { + this.envs = Collections.emptyMap(); + this.commandWithArguments = commandWithArguments; + } + + Command(Map envs, String commandWithArguments) { + this.envs = envs; + this.commandWithArguments = commandWithArguments; + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManagerType.java b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManagerType.java new file mode 100644 index 00000000..c3af7c78 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/quinoa/deployment/packagemanager/types/PackageManagerType.java @@ -0,0 +1,50 @@ +package io.quarkiverse.quinoa.deployment.packagemanager.types; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum PackageManagerType { + // Order matters for detection + PNPM("pnpm", "pnpm-lock.yaml", "install --frozen-lockfile"), + NPM("npm", "package-lock.json", "ci"), + YARN("yarn", "yarn.lock", "install --frozen-lockfile"), + ; + + private static final Map TYPES = Arrays.stream(values()).sequential() + .collect(Collectors.toMap(PackageManagerType::getBinary, Function.identity(), (x, y) -> x, LinkedHashMap::new)); + + private final String binary; + private final String lockFile; + + private final String ciCommand; + + PackageManagerType(String binary, String lockFile, String ciCommand) { + this.binary = binary; + this.lockFile = lockFile; + this.ciCommand = ciCommand; + } + + public String getBinary() { + return binary; + } + + public String getLockFile() { + return lockFile; + } + + public String ciCommand() { + return ciCommand; + } + + public static PackageManagerType resolveType(String binary) { + for (Map.Entry e : TYPES.entrySet()) { + if (binary.contains(e.getKey())) { + return e.getValue(); + } + } + throw new UnsupportedOperationException("Unsupported package manager binary: " + binary); + } +} \ No newline at end of file diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaFrozenLockfileConfigTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaCIConfigTest.java similarity index 75% rename from deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaFrozenLockfileConfigTest.java rename to deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaCIConfigTest.java index d19660ad..9276b6d9 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaFrozenLockfileConfigTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaCIConfigTest.java @@ -7,24 +7,24 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; -public class QuinoaFrozenLockfileConfigTest { +public class QuinoaCIConfigTest { - private static final String NAME = "frozen-lockfile"; + private static final String NAME = "ci"; @RegisterExtension static final QuarkusUnitTest config = QuinoaQuarkusUnitTest.create(NAME) .initialLockfile(PackageManagerType.YARN.getLockFile()) .ci(null) .toQuarkusUnitTest() - .overrideConfigKey("quarkus.quinoa.frozen-lockfile", "true") + .overrideConfigKey("quarkus.quinoa.ci", "true") .assertLogRecords(l -> assertThat(l) - .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager install command: %s") && + .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager ci command: %s") && s.getParameters()[0].equals( - systemBinary(PackageManagerType.YARN.getCommand()) + " install --frozen-lockfile"))); + systemBinary(PackageManagerType.YARN.getBinary()) + " install --frozen-lockfile"))); @Test public void testQuinoa() { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallPrependBinaryTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallCommandOverrideTest.java similarity index 92% rename from deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallPrependBinaryTest.java rename to deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallCommandOverrideTest.java index 1a8b54c4..92e4f388 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallPrependBinaryTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerInstallCommandOverrideTest.java @@ -12,8 +12,8 @@ import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; -public class QuinoaPackageManagerInstallPrependBinaryTest { - private static final String NAME = "package-manager-install-prepend-binary"; +public class QuinoaPackageManagerInstallCommandOverrideTest { + private static final String NAME = "package-manager-install-command-override"; public static final String INSTALL_DIR = "target/node-" + NAME; @RegisterExtension @@ -21,7 +21,6 @@ public class QuinoaPackageManagerInstallPrependBinaryTest { .overrideConfigKey("quarkus.quinoa.package-manager-install", "true") .overrideConfigKey("quarkus.quinoa.package-manager-install.node-version", "18.17.0") .overrideConfigKey("quarkus.quinoa.package-manager-install.install-dir", INSTALL_DIR) - .overrideConfigKey("quarkus.quinoa.package-manager-command.prepend-binary", "true") .overrideConfigKey("quarkus.quinoa.package-manager-command.build", "run build-something") .overrideConfigKey("quarkus.quinoa.package-manager-command.build-env.BUILD", "yeahhh") .assertLogRecords(l -> assertThat(l) diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectNPMTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectNPMTest.java index 435f8376..1b471d2b 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectNPMTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectNPMTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; @@ -22,7 +22,7 @@ public class QuinoaPackageManagerLockfileDetectNPMTest { .toQuarkusUnitTest() .assertLogRecords(l -> assertThat(l) .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && - s.getParameters()[0].equals(systemBinary(PackageManagerType.NPM.getCommand()) + " run build"))); + s.getParameters()[0].equals(systemBinary(PackageManagerType.NPM.getBinary()) + " run build"))); @Test public void testQuinoa() { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectPNPMTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectPNPMTest.java index 7b6e1382..637b7ff5 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectPNPMTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectPNPMTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; @@ -22,7 +22,7 @@ public class QuinoaPackageManagerLockfileDetectPNPMTest { .toQuarkusUnitTest() .assertLogRecords(l -> assertThat(l) .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && - s.getParameters()[0].equals(systemBinary(PackageManagerType.PNPM.getCommand()) + " run build"))); + s.getParameters()[0].equals(systemBinary(PackageManagerType.PNPM.getBinary()) + " run build"))); @Test public void testQuinoa() { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectYarnTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectYarnTest.java index a59f97f3..371a5d0b 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectYarnTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerLockfileDetectYarnTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; @@ -22,7 +22,7 @@ public class QuinoaPackageManagerLockfileDetectYarnTest { .toQuarkusUnitTest() .assertLogRecords(l -> { assertThat(l).anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && - s.getParameters()[0].equals(systemBinary(PackageManagerType.YARN.getCommand()) + " run build")); + s.getParameters()[0].equals(systemBinary(PackageManagerType.YARN.getBinary()) + " run build")); }); @Test diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerNPMOverrideBuildEnvTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerNPMOverrideBuildEnvTest.java index 0c45c111..517a419c 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerNPMOverrideBuildEnvTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerNPMOverrideBuildEnvTest.java @@ -14,7 +14,7 @@ public class QuinoaPackageManagerNPMOverrideBuildEnvTest { private static final String NAME = "package-manager-npm-override-build"; - private static final String BUILD_COMMAND = systemBinary("npm") + " run build-something"; + private static final String BUILD_COMMAND = "run build-something"; @RegisterExtension static final QuarkusUnitTest config = QuinoaQuarkusUnitTest.create(NAME).toQuarkusUnitTest() @@ -22,7 +22,7 @@ public class QuinoaPackageManagerNPMOverrideBuildEnvTest { .overrideConfigKey("quarkus.quinoa.package-manager-command.build-env.BUILD", "develop") .assertLogRecords(l -> assertThat(l) .anyMatch(s -> s.getMessage().equals("Running Quinoa package manager build command: %s") && - s.getParameters()[0].equals(BUILD_COMMAND))); + s.getParameters()[0].equals(systemBinary("npm") + " " + BUILD_COMMAND))); @Test public void testQuinoa() { diff --git a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerSetYarnOtherConfigTest.java b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerSetYarnOtherConfigTest.java index 891c255a..0e0c1bf5 100644 --- a/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerSetYarnOtherConfigTest.java +++ b/deployment/src/test/java/io/quarkiverse/quinoa/test/QuinoaPackageManagerSetYarnOtherConfigTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType; +import io.quarkiverse.quinoa.deployment.packagemanager.types.PackageManagerType; import io.quarkiverse.quinoa.deployment.testing.QuinoaQuarkusUnitTest; import io.quarkus.test.QuarkusUnitTest; diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 102b5e39..dccae94b 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1 +1,9 @@ -* xref:index.adoc[Quarkus - Quinoa] +include::./includes/attributes.adoc[] +:config-file: application.properties + +* xref:index.adoc[Getting started] +* xref:main-concepts.adoc[Main Concepts] +* xref:web-frameworks.adoc[Web Frameworks] +* xref:advanced-guides.adoc[Advanced Guides] +* xref:advanced-guides.adoc[Testing] +* xref:config-reference.adoc[Config Reference] diff --git a/docs/modules/ROOT/pages/advanced-guides.adoc b/docs/modules/ROOT/pages/advanced-guides.adoc new file mode 100644 index 00000000..2d40f04d --- /dev/null +++ b/docs/modules/ROOT/pages/advanced-guides.adoc @@ -0,0 +1,201 @@ += Quarkus Quinoa - Advanced Guides + +[#how-to] +== 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 link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.build-dir[Build Dir]). +[source,json] +---- + "scripts": { + "start": "[start the Web UI live coding server]", + "build": "[build the Web UI]", + "test": "[test the Web UI]" + }, +---- + +NOTE: 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. + +[#build-mode] +You can differentiate development from production builds using the environment variable `NODE_ENV` (`production`/`development`). https://www.npmjs.com/package/by-node-env[by-node-env] can help you if you have different build commands: +[source,json] +---- +"scripts": { + "build": "by-node-env", + "build:development": "...", + "build:production": "...", +}, +"devDependencies": { + "by-node-env": "~2.0.1" +} +---- + +[#package-manager] +=== Package manager + +[#package-manager-install] +Quinoa can be configured to install NodeJS and NPM in the project directory: +[source,properties] +---- +quarkus.quinoa.package-manager-install=true <1> +quarkus.quinoa.package-manager-install.node-version=18.17.0 <2> +---- + +<1> Enable package manager install +<2> Define the version of NodeJS to install + +NOTE: By default, NodeJS and NPM will be installed in `pass:[{project-dir}]/.quinoa/` (can be link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.package-manager-install.install-dir[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* + +NOTE: 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). + +[#install-packages] +=== 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`. + +[#frozen-lockfile] +NOTE: 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] +=== 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=pass:[${mode}]` (https://quarkus.io/guides/lifecycle#launch-modes[`dev`, `test` or `prod`]) + +*Test:* + +`(npm|pnpm|yarn) test`, with environment `CI=true` + +*Dev:* + +`(npm|pnpm|yarn) start`, with environment `BROWSER=none` + +[#override-commands] +=== 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 link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.package-manager-command.install[package manager command configuration]: + +[source,properties] +---- +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 link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.package-manager-command.build-env-build-env[listed commands]. + +WARNING: Using custom commands will override `quarkus.quinoa.package-manager` and `quarkus.quinoa.frozen-lockfile`. + +WARNING: 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). + + +[#dev-server] +=== UI live-coding dev server (proxy mode) + +Quinoa provides two options for live-coding: + +* Delegate to the <>. 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). + +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: +[source,properties] +---- +quarkus.quinoa.dev-server.port=3000 +---- + +NOTE: Quinoa relies on the dev server returning a 404 when the file is not found (See link:main-concepts.adoc#how-dev-server[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 https://github.com/quarkiverse/quarkus-quinoa/issues/91[#91]). Another option, when possible, is to use link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.ignored-path-prefixes[Ignored Path Prefixes]. + +[#spa-routing] +=== 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 https://developer.mozilla.org/en-US/docs/Web/API/History[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: +[source,properties] +---- +quarkus.quinoa.enable-spa-routing=true +---- + +NOTE: 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`. + +WARNING: 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): +[source,java] +---- +@ApplicationScoped +public class SPARouting { + private static final String[] PATH_PREFIXES = { "/api/", "/q/" }; + private static final Predicate 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(); + } + }); + } +} +---- + +[#headers] +=== Http Headers + +It's very common to set up headers for caching on static resources, for example React proposes https://create-react-app.dev/docs/production-build/#static-file-caching[this configuration]: + +To configure Quarkus with those headers : +[source,properties] +---- +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] +=== Http Compression + +To enable server Http compression: +[source,properties] +---- +quarkus.http.enable-compression=true +---- + +=== 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 link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.frozen-lockfile[frozen lockfile]. + +On compatible CIs, don't forget to enable the Maven/Gradle and NPM/Yarn repository caching. + + diff --git a/docs/modules/ROOT/pages/config-reference.adoc b/docs/modules/ROOT/pages/config-reference.adoc new file mode 100644 index 00000000..fe4a201b --- /dev/null +++ b/docs/modules/ROOT/pages/config-reference.adoc @@ -0,0 +1,5 @@ += Quarkus image:logo.svg[width=25em] Web Bundler - Configuration Reference + +include::./includes/attributes.adoc[] + +include::includes/quarkus-quinoa.adoc[leveloffset=+1, opts=optional] diff --git a/docs/modules/ROOT/pages/includes/attributes.adoc b/docs/modules/ROOT/pages/includes/attributes.adoc index 642c3acf..bcdee064 100644 --- a/docs/modules/ROOT/pages/includes/attributes.adoc +++ b/docs/modules/ROOT/pages/includes/attributes.adoc @@ -1,6 +1,7 @@ -:quarkus-version: 3.3.0 +:quarkus-version: 3.2.6.Final :quarkus-quinoa-version: 2.1.0 :maven-version: 3.8.1+ +:extension-status: stable :quarkus-org-url: https://github.com/quarkusio :quarkus-base-url: {quarkus-org-url}/quarkus diff --git a/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc b/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc index ea094b0f..779a2d91 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-quinoa.adoc @@ -15,8 +15,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa]]`link:#q [.description] -- -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). - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA+++[] endif::add-copy-button-to-env-var[] @@ -24,7 +22,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA+++` endif::add-copy-button-to-env-var[] --|boolean -|`disabled in test mode` +|`enabled (disabled in test mode)` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.just-build]]`link:#quarkus-quinoa_quarkus.quinoa.just-build[quarkus.quinoa.just-build]` @@ -32,8 +30,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.just-buil [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_JUST_BUILD+++[] endif::add-copy-button-to-env-var[] @@ -49,8 +45,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.ui-dir]]` [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_UI_DIR+++[] endif::add-copy-button-to-env-var[] @@ -66,8 +60,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.build-dir [.description] -- -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 - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_BUILD_DIR+++[] endif::add-copy-button-to-env-var[] @@ -75,7 +67,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_BUILD_DIR+++` endif::add-copy-button-to-env-var[] --|string -|`build/` +|`framework detection with fallback to 'build/'` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager[quarkus.quinoa.package-manager]` @@ -83,8 +75,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER+++[] endif::add-copy-button-to-env-var[] @@ -92,7 +82,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER+++` endif::add-copy-button-to-env-var[] --|string -|`auto-detected with lockfile` +|`auto-detected based on lockfile falling back to 'npm'` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install[quarkus.quinoa.package-manager-install]` @@ -117,7 +107,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- -The directory where NodeJS should be installed, it will be installed in a node/ sub-directory. Default is $++{++project.root++}++/.quinoa +The directory where NodeJS should be installed (relative to the project root), It will be installed in a 'node/' subdirectory of this. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_INSTALL_DIR+++[] @@ -146,29 +136,12 @@ endif::add-copy-button-to-env-var[] | -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.node-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.node-download-root[quarkus.quinoa.package-manager-install.node-download-root]` - - -[.description] --- -Where to download NodeJS from. - -ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_DOWNLOAD_ROOT+++[] -endif::add-copy-button-to-env-var[] -ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_DOWNLOAD_ROOT+++` -endif::add-copy-button-to-env-var[] ---|string -|`https://nodejs.org/dist/` - - a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.npm-version]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.npm-version[quarkus.quinoa.package-manager-install.npm-version]` [.description] -- -The NPM version to install. By default, the version is provided by NodeJS. +The NPM version to install and use. By default, the version is provided by NodeJS. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NPM_VERSION+++[] @@ -177,7 +150,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NPM_VERSION+++` endif::add-copy-button-to-env-var[] --|string -|`provided` +|`'provided' means it will use the NPM embedded in NodeJS` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.npm-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.npm-download-root[quarkus.quinoa.package-manager-install.npm-download-root]` @@ -197,89 +170,89 @@ endif::add-copy-button-to-env-var[] |`https://registry.npmjs.org/npm/-/` -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-version]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-version[quarkus.quinoa.package-manager-install.pnpm-version]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.node-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.node-download-root[quarkus.quinoa.package-manager-install.node-download-root]` [.description] -- -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. +Where to download NodeJS from. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_VERSION+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_DOWNLOAD_ROOT+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_VERSION+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_NODE_DOWNLOAD_ROOT+++` endif::add-copy-button-to-env-var[] --|string -| +|`https://nodejs.org/dist/` -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-download-root[quarkus.quinoa.package-manager-install.pnpm-download-root]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-version]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-version[quarkus.quinoa.package-manager-install.yarn-version]` [.description] -- -Where to download PNPM from. +Install and use Yarn as package manager with this version. This is ignored if the npm-version is defined. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_DOWNLOAD_ROOT+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_VERSION+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_DOWNLOAD_ROOT+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_VERSION+++` endif::add-copy-button-to-env-var[] --|string -|`https://registry.npmjs.org/pnpm/-/` +| -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-version]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-version[quarkus.quinoa.package-manager-install.yarn-version]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-download-root[quarkus.quinoa.package-manager-install.yarn-download-root]` [.description] -- -The YARN version to install. If the version is set and NPM Version is not set, then this version will attempt to be downloaded. +Where to download YARN from. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_VERSION+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_DOWNLOAD_ROOT+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_VERSION+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_DOWNLOAD_ROOT+++` endif::add-copy-button-to-env-var[] --|string -| +|`https://github.com/yarnpkg/yarn/releases/download/` -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.yarn-download-root[quarkus.quinoa.package-manager-install.yarn-download-root]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-version]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-version[quarkus.quinoa.package-manager-install.pnpm-version]` [.description] -- -Where to download YARN from. +Install and use PNPM as package manager with this version. This is ignored if the npm-version or the yarn-version is defined. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_DOWNLOAD_ROOT+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_VERSION+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_YARN_DOWNLOAD_ROOT+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_VERSION+++` endif::add-copy-button-to-env-var[] --|string -|`https://github.com/yarnpkg/yarn/releases/download/` +| -a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-command.prepend-binary]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.prepend-binary[quarkus.quinoa.package-manager-command.prepend-binary]` +a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-download-root]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.pnpm-download-root[quarkus.quinoa.package-manager-install.pnpm-download-root]` [.description] -- -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. +Where to download PNPM from. ifdef::add-copy-button-to-env-var[] -Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_PREPEND_BINARY+++[] +Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_DOWNLOAD_ROOT+++[] endif::add-copy-button-to-env-var[] ifndef::add-copy-button-to-env-var[] -Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_PREPEND_BINARY+++` +Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_INSTALL_PNPM_DOWNLOAD_ROOT+++` endif::add-copy-button-to-env-var[] ---|boolean -|`false` +--|string +|`https://registry.npmjs.org/pnpm/-/` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-command.install]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.install[quarkus.quinoa.package-manager-command.install]` @@ -287,7 +260,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- -Custom command for installing all dependencies. e.g. «npm ci --cache $CACHE_DIR/.npm --prefer-offline» +Custom command for installing all dependencies. e.g. «ci --cache $CACHE_DIR/.npm --prefer-offline» ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL+++[] @@ -313,7 +286,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD+++` endif::add-copy-button-to-env-var[] --|string -| +|`build` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-command.test]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.test[quarkus.quinoa.package-manager-command.test]` @@ -330,7 +303,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST+++` endif::add-copy-button-to-env-var[] --|string -| +|`test` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-manager-command.dev]]`link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.dev[quarkus.quinoa.package-manager-command.dev]` @@ -347,7 +320,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV+++` endif::add-copy-button-to-env-var[] --|string -| +|`framework detection with fallback to 'start'` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.index-page]]`link:#quarkus-quinoa_quarkus.quinoa.index-page[quarkus.quinoa.index-page]` @@ -355,8 +328,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.index-pag [.description] -- -Name of the index page. If not set, "index.html" will be used. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_INDEX_PAGE+++[] endif::add-copy-button-to-env-var[] @@ -372,8 +343,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.run-tests [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_RUN_TESTS+++[] endif::add-copy-button-to-env-var[] @@ -389,8 +358,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.frozen-lo [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_FROZEN_LOCKFILE+++[] endif::add-copy-button-to-env-var[] @@ -406,8 +373,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.force-ins [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_FORCE_INSTALL+++[] endif::add-copy-button-to-env-var[] @@ -423,8 +388,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.enable-sp [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_ENABLE_SPA_ROUTING+++[] endif::add-copy-button-to-env-var[] @@ -440,8 +403,6 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.ignored-p [.description] -- -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. - ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_IGNORED_PATH_PREFIXES+++[] endif::add-copy-button-to-env-var[] @@ -449,7 +410,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_IGNORED_PATH_PREFIXES+++` endif::add-copy-button-to-env-var[] --|list of string -| +|`ignore values configured by 'quarkus.resteasy-reactive.path', 'quarkus.resteasy.path' and 'quarkus.http.non-application-root-path'` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server[quarkus.quinoa.dev-server]` @@ -457,7 +418,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-serve [.description] -- -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. +Enable external dev server (live coding). If the "dev-server.port" config is not detected or defined it will be disabled. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER+++[] @@ -500,7 +461,7 @@ ifndef::add-copy-button-to-env-var[] Environment variable: `+++QUARKUS_QUINOA_DEV_SERVER_PORT+++` endif::add-copy-button-to-env-var[] --|int -| +|`framework detection or fallback to empty` a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-server.host]]`link:#quarkus-quinoa_quarkus.quinoa.dev-server.host[quarkus.quinoa.dev-server.host]` @@ -508,7 +469,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-serve [.description] -- -Host of the server to forward requests to. "localhost" is the default +Host of the server to forward requests to. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_HOST+++[] @@ -525,7 +486,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-serve [.description] -- -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. +After start, Quinoa wait for the external dev server. by sending GET requests to this path waiting for a 200 status. If forced empty, Quinoa will not check if the dev server is up. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_CHECK_PATH+++[] @@ -559,7 +520,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-serve [.description] -- -Timeout in ms for the dev server to be up and running. If not set the default is ~30000ms. +Timeout in ms for the dev server to be up and running. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_CHECK_TIMEOUT+++[] @@ -576,7 +537,7 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.dev-serve [.description] -- -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. +Enable external dev server live coding logs. This is not enabled by default because most dev servers display compilation errors directly in the browser. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_DEV_SERVER_LOGS+++[] @@ -593,6 +554,8 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- +Environment variables for install command execution. + ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_INSTALL_ENV+++[] endif::add-copy-button-to-env-var[] @@ -608,6 +571,8 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- +Environment variables for build command execution. + ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_BUILD_ENV+++[] endif::add-copy-button-to-env-var[] @@ -623,6 +588,8 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- +Environment variables for test command execution. + ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_TEST_ENV+++[] endif::add-copy-button-to-env-var[] @@ -638,6 +605,8 @@ a|icon:lock[title=Fixed at build time] [[quarkus-quinoa_quarkus.quinoa.package-m [.description] -- +Environment variables for development command execution. + ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_QUINOA_PACKAGE_MANAGER_COMMAND_DEV_ENV+++[] endif::add-copy-button-to-env-var[] diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index d3326683..ab1b50c4 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -1,5 +1,4 @@ -= Quarkus - Quinoa -:extension-status: stable += Quarkus Quinoa - Getting Started include::./includes/attributes.adoc[] @@ -7,42 +6,11 @@ Quinoa is a Quarkus extension which eases the development, the build and serving 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) - -image::quinoa-build.png[Quinoa Build] - -NOTE: packages are installed by Quinoa before the build when needed (i.e `npm install`). See link:#install-packages[Packages installation]. Quinoa is pre-configured to work with your favorite link:#package-manager[package manager] (npm, yarn or pnpm). - -=== Runtime for production mode - -When running jar or binary in production mode: - -image::quinoa-runtime-prod.png[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. - -NOTE: You can differentiate the build for link:#build-mode[dev mode]. e.g to disable minification. - -[#how-dev-server] -=== Runtime for proxied live-coding - -When running dev-mode (e.g with npm on port 3000): - -image::quinoa-proxy-dev.png[Quinoa Proxy Dev] - -NOTE: Quarkus live-coding will keep watching for the backend changes as usual. - -See link:#dev-server[Enable the proxied live coding]. - == Prerequisite * Create or use an existing Quarkus application * Add the Quinoa extension -* Install NodeJS (https://nodejs.org/) or make sure Quinoa is link:#package-manager-install[configured] to install it. +* Install NodeJS (https://nodejs.org/) or make sure Quinoa is link:config-reference.adoc#quarkus-quinoa_quarkus.quinoa.package-manager-install[configured] to install it. [#installation] == Installation @@ -85,491 +53,40 @@ quarkus ext add io.quarkiverse.quinoa:quarkus-quinoa [#getting-started] == 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. -[source,properties] ----- -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 <>, <>, 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 <>. 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: -[source,shell] ----- -$ 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 - -[source,shell] ----- -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' ----- - -WARNING: 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] -== 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 <>. -[source,json] ----- - "scripts": { - "start": "[start the Web UI live coding server]", - "build": "[build the Web UI]", - "test": "[test the Web UI]" - }, ----- - -NOTE: 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. - -[#build-mode] -You can differentiate development from production builds using the environment variable `NODE_ENV` (`production`/`development`). https://www.npmjs.com/package/by-node-env[by-node-env] can help you if you have different build commands: -[source,json] ----- -"scripts": { - "build": "by-node-env", - "build:development": "...", - "build:production": "...", -}, -"devDependencies": { - "by-node-env": "~2.0.1" -} ----- - -[#package-manager] -=== Package manager - -[#package-manager-install] -Quinoa can be configured to install NodeJS and NPM in the project directory: -[source,properties] ----- -quarkus.quinoa.package-manager-install=true <1> -quarkus.quinoa.package-manager-install.node-version=18.18.0 <2> ----- - -<1> Enable package manager install -<2> Define the version of NodeJS to install - -NOTE: By default, NodeJS and NPM will be installed in `pass:[{project-dir}]/.quinoa/` (can be link:#quarkus-quinoa_quarkus.quinoa.package-manager-install.install-dir[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* - -NOTE: 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). - -[#install-packages] -=== 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`. - -[#frozen-lockfile] -NOTE: 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. +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`. -[#package-manager-commands] -=== Package manager commands +NOTE: The location is configurable, the directory could be outside the Quarkus project as long as the files are available at build time. -By default, the following commands and environment variables are used in the different faces for each of the supported package managers. +You may use an existing Web UI or generate a starter application from any existing Node based xref:web-frameworks.adoc[Web Framework]. Some Web Frameworks are detected and preconfigured with sensible defaults. If your Web Framework is not part of the detected frameworks or if you changed the configuration, you can always configure Quinoa to work with it. -*Install:* +The key points for Quinoa are: -* `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=pass:[${mode}]` (https://quarkus.io/guides/lifecycle#launch-modes[`dev`, `test` or `prod`]) - -*Test:* - -`(npm|pnpm|yarn) test`, with environment `CI=true` - -*Dev:* - -`(npm|pnpm|yarn) start`, with environment `BROWSER=none` - -[#override-commands] -=== 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 link:#quarkus-quinoa_quarkus.quinoa.package-manager-command.install[package manager command configuration]: +- the `package.json` scripts: `build`, optionally `start/dev` and `test` +- the directory where the web files (index.html, scripts, ...) are generated: dist, build, ... +- the port used by the dev-server -[source,properties] ----- -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 link:#package-manager-commands[listed commands]. - -WARNING: Using custom commands will override `quarkus.quinoa.package-manager` and `quarkus.quinoa.frozen-lockfile`. - -WARNING: 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). - -[#dev-server] -=== 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: +.application.properties [source,properties] ---- quarkus.quinoa.dev-server.port=3000 +quarkus.quinoa.build-dir=dist ---- -NOTE: Quinoa relies on the dev server returning a 404 when the file is not found (See link:#how-dev-server[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 https://github.com/quarkiverse/quarkus-quinoa/issues/91[#91]). Another option, when possible, is to use <>. - -[#spa-routing] -=== 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 https://developer.mozilla.org/en-US/docs/Web/API/History[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: -[source,properties] ----- -quarkus.quinoa.enable-spa-routing=true ----- - -NOTE: 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`. - -WARNING: 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): -[source,java] ----- -@ApplicationScoped -public class SPARouting { - private static final String[] PATH_PREFIXES = { "/api/", "/q/" }; - private static final Predicate 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(); - } - }); - } -} ----- - -[#headers] -=== Http Headers - -It's very common to set up headers for caching on static resources, for example React proposes https://create-react-app.dev/docs/production-build/#static-file-caching[this configuration]: - -To configure Quarkus with those headers : -[source,properties] ----- -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] -=== Http Compression - -To enable server Http compression: -[source,properties] ----- -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: -[source,xml,subs=attributes+] ----- - - io.quarkiverse.quinoa - quarkus-quinoa-testing - {quarkus-quinoa-version} - test - ----- - -In order to enable the Web UI (build and serve) in a particular test, you can use the `Enable` test profile: - -[source,java] ----- -@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: - -[source,java] ----- -@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 https://docs.quarkiverse.io/quarkus-playwright/dev/[Quarkus Playright]: -[source,java] ----- -import com.microsoft.playwright.BrowserContext; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Response; -import io.quarkiverse.playwright.WithPlaywright; -import io.quarkiverse.playwright.InjectPlaywright; -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) -@WithPlaywright -public class MyWebUITest { - @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 link:#frozen-lockfile[frozen lockfile]. - -On compatible CIs, don't forget to enable the Maven/Gradle and NPM/Yarn repository caching. - -[#frameworks] -== Frameworks - -Quinoa attempts to automatically determine which framework you are using by reading your package.json file and auto-configure settings for that framework. -For developers, this provides more "convention over configuration" approach for a smoother experience. - -[#react] -=== React - -App created by https://create-react-app.dev/docs/getting-started[Create React App], https://github.com/timarney/react-app-rewired[React App Rewired] and https://craco.js.org/[CRACO] are compatible without any change. - -[#vue] -=== Vue - -App created by https://cli.vuejs.org/guide/cli-service.html[Vue CLI Service] or https://github.com/vuejs/create-vue[Create Vue] are compatible without any change. - - -[#angular] -=== Angular - -App created by `ng` (https://angular.io/guide/setup-local) require a tiny bit of configuration: - -To enable Angular live coding server, you need to edit the package.json start script: - -[source,json] ----- - "scripts": { - ... - "start": "ng serve --disable-host-check" - }, ----- - -If you want to use the Angular tests (instead of Playwright from the @QuarkusTest): - -Change the package.json test script: -[source,json] ----- - "scripts": { - ... - "test": "ng test -- --no-watch --no-progress --browsers=ChromeHeadlessCI" - }, ----- - -Edit the karma.conf.js: -[source,javascript] ----- - browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'], - customLaunchers: { - ChromeHeadlessCI: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } -}, ----- - -[#nextjs] -=== Next.js -Any app created with https://nextjs.org/[Next.js] should work with Quinoa after the following changes: - -In application.properties add: -[source,properties] ----- -%dev.quarkus.quinoa.index-page=/ ----- - -In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html". - -Update this script in package.json: -[source,json] ----- - "scripts": { - ... - "build": "next build && next export", - } ----- - -[#svelte] -=== Svelte (using Vite) -Any app created with https://svelte.dev/docs/introduction#start-a-new-project-alternatives-to-sveltekit[Vite Svelte] are compatible without any change and Hot Module Replacement (HMR) should work by default similar to the link:#vite[Vite] section below - -[#svelte-kit] -=== Svelte Kit -Any app created with https://kit.svelte.dev/[Svelte Kit] does NOT work in dev mode as it has some complex server side interaction. It will work in production mode though with the following chnages: - -Install Svelte Static adapter: +Start the Quarkus live coding: [source,shell] ---- -npm i -D @sveltejs/adapter-static ----- - -Configure `svelte.config` file with the following changes: - -[source,json] ----- -import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/kit/vite'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - fallback: 'index.html' - }) - } -}; - -export default config; +$ quarkus dev ---- -In `application.properties` add: -[source,properties] +Build your app: +[source,shell] ---- -quarkus.quinoa.dev-server=false -quarkus.quinoa.build-dir=build +$ quarkus build +$ java -jar target/quarkus-app/quarkus-run.jar ---- +*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 -[#vite] -=== Vite -Any app created with https://vitejs.dev/guide/[Vite] are compatible without any change and Hot Module Replacement (HMR) should work by default. - -[#nuxt] -=== Nuxt -Any app created with https://v2.nuxt.com/docs/get-started/installation#set-up-your-project[Nuxt] are compatible without any change and Hot Module Replacement (HMR) should work by default. - -[#solidstart] -=== Solid Start -Any app created with https://start.solidjs.com/getting-started/what-is-solidstart[SolidStart] are compatible without any change and Hot Module Replacement (HMR) should work by default. It is based on link:#vite[Vite]. - -[#astro] -=== Astro -Any app created with https://astro.build/[Astro] are compatible without any change and Hot Module Replacement (HMR) should work by default. It is based on link:#vite[Vite]. - -[#gatsby] -=== Gatsby -Any app created with https://www.gatsbyjs.com/docs/quick-start/[Gatsby] are compatible without any change and Hot Module Replacement (HMR) should work by default. It is based on link:#vite[Vite]. - -[#ember] -=== Ember -Any app created with https://guides.emberjs.com/release/getting-started/[Ember] are compatible without any change and Hot Module Replacement (HMR) should work by default. - -[#aurelia] -=== Aurelia -App created with https://aurelia.io/docs/tutorials/creating-a-todo-app#setup[Aurelia] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#polymer] -=== Polymer -App created with https://polymer-library.polymer-project.org/3.0/docs/first-element/intro[Polymer] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#qwik] -=== Qwik -App created with https://qwik.builder.io/docs/getting-started/[Qwik] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#cycle] -=== Cycle -App created with https://cycle.js.org/getting-started.html[Cycle] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#riotjs] -=== Riot.js -App created with https://riot.js.org/[Riot.js] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#midwayjs] -=== Midway.js -App created with https://www.midwayjs.org/[Midway.js] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[#refine] -=== Refine -App created with https://refine.dev/docs/getting-started/quickstart/[Refine] are compatible without any changes and Hot Module Replacement (HMR) should work by default. - -[[extension-configuration-reference]] -== Extension Configuration Reference - -include::includes/quarkus-quinoa.adoc[leveloffset=+1, opts=optional] +WARNING: 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.* diff --git a/docs/modules/ROOT/pages/main-concepts.adoc b/docs/modules/ROOT/pages/main-concepts.adoc new file mode 100644 index 00000000..0d99876d --- /dev/null +++ b/docs/modules/ROOT/pages/main-concepts.adoc @@ -0,0 +1,34 @@ += Quarkus Quinoa - Main Concepts + +include::./includes/attributes.adoc[] + +== How it works + +=== The Quinoa build (using npm) + +image::quinoa-build.png[Quinoa Build] + +NOTE: packages are installed by Quinoa before the build when needed (i.e `npm install`). See link:#install-packages[Packages installation]. Quinoa is pre-configured to work with your favorite link:#package-manager[package manager] (npm, yarn or pnpm). + +=== Runtime for production mode + +When running jar or binary in production mode: + +image::quinoa-runtime-prod.png[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. + +NOTE: You can differentiate the build for link:#build-mode[dev mode]. e.g to disable minification. + +[#how-dev-server] +=== Runtime for proxied live-coding + +When running dev-mode (e.g with npm on port 3000): + +image::quinoa-proxy-dev.png[Quinoa Proxy Dev] + +NOTE: Quarkus live-coding will keep watching for the backend changes as usual. + +See link:#dev-server[Enable the proxied live coding]. diff --git a/docs/modules/ROOT/pages/testing.adoc b/docs/modules/ROOT/pages/testing.adoc new file mode 100644 index 00000000..244afab1 --- /dev/null +++ b/docs/modules/ROOT/pages/testing.adoc @@ -0,0 +1,88 @@ += Quarkus Quinoa - Testing + +include::./includes/attributes.adoc[] + +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 (it also includes https://docs.quarkiverse.io/quarkus-playwright/dev/[Quarkus Playwright]) : +[source,xml,subs=attributes+] +---- + + io.quarkiverse.quinoa + quarkus-quinoa-testing + {quarkus-quinoa-version} + test + +---- + +In order to enable the Web UI (build and serve) in a particular test, you can use the `Enable` test profile: + +[source,java] +---- +@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: + +[source,java] +---- +@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 https://docs.quarkiverse.io/quarkus-playwright/dev/[Quarkus Playwright]: +[source,java] +---- +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Response; +import io.quarkiverse.playwright.WithPlaywright; +import io.quarkiverse.playwright.InjectPlaywright; +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) +@WithPlaywright +public class MyWebUITest { + @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); + } +} +---- diff --git a/docs/modules/ROOT/pages/web-frameworks.adoc b/docs/modules/ROOT/pages/web-frameworks.adoc new file mode 100644 index 00000000..87de1469 --- /dev/null +++ b/docs/modules/ROOT/pages/web-frameworks.adoc @@ -0,0 +1,325 @@ += Quarkus Quinoa - Web Frameworks + +include::./includes/attributes.adoc[] + +Quinoa attempts to automatically determine which framework you are using by reading your package.json file and auto-configure settings for that framework. When possible, if some changes are required in the Web UI, it will try to help you configure it. + +For developers, this provides more "convention over configuration" approach for a smoother experience. + +== Detected Frameworks + +[cols="^.^2,^.^1,3,1,1"] +|=== +|Name|Preconfigured|Starter|Guides|Config + +a| +[#react] +=== React +|✓ +a| +[source,bash] +---- +npx create-react-app my-app +---- +a| +- https://create-react-app.dev/docs/getting-started[Create React App] +- https://github.com/timarney/react-app-rewired[React App Rewired] +- https://craco.js.org/[CRACO] +| + +a| +[#vue] +=== Vue +|✓ +a| +[source,bash] +---- +npm create vue@latest +---- +a| +- https://cli.vuejs.org/guide/cli-service.html[Vue CLI Service] +- https://github.com/vuejs/create-vue[Create Vue] +| + +a| +[#angular] +=== Angular +|✓* +a| +[source,bash] +---- +npm install -g @angular/cli +ng new my-first-project +---- +a| +- https://angular.io/guide/setup-local[ng] +| <> + +a| +[#nextjs] +=== Next.js +|~ +a| +[source,bash] +---- +npx create-next-app@latest +---- +a| +- https://nextjs.org/docs[Next] +| <> + +a| +[#vite] +=== Vite +|✓ +a| +[source,bash] +---- +npm create vite@latest +---- +a| +- https://vitejs.dev/guide/[Vite] +| + +a| +[#svelte-kit] +=== Svelte Kit +|~ +a| +[source,bash] +---- +npm create svelte@latest myapp +---- +a| +- https://kit.svelte.dev/[Svelte Kit] +| <> + +a| +[#nuxt] +=== Nuxt +|✓ +a| +[source,bash] +---- +npm init nuxt-app +---- +a| +- https://v2.nuxt.com/docs/get-started/installation#set-up-your-project[Nuxt] +| + +a| +[#solidstart] +=== Solid Start +|✓ +a| +[source,bash] +---- +mkdir my-app && cd my-app +npm init solid@latest +---- +a| +- https://start.solidjs.com/getting-started/what-is-solidstart[SolidStart] +| + +a| +[#astro] +=== Astro +|✓ +a| +[source,bash] +---- +npm create astro@latest +---- +a| +- https://astro.build/[Astro] +| + +a| +[#gatsby] +=== Gatsby +|✓ +a| +[source,bash] +---- +npm init gatsby@latest +---- +a| +- https://www.gatsbyjs.com/docs/quick-start/[Gatsby] +| + +a| +[#ember] +=== Ember +|✓ +a| +[source,bash] +---- +npm install -g ember-cli +ember new ember-quickstart +---- +a| +- https://guides.emberjs.com/release/getting-started/[Ember] +| + +a| +[#aurelia] +=== Aurelia +|✓ +a| +[source,bash] +---- +npm install -g aurelia-cli +au new +---- +a| +- https://aurelia.io/docs/tutorials/creating-a-todo-app#setup[Aurelia] +| + +a| +[#polymer] +=== Polymer +|✓ +a| +[source,bash] +---- +npm install -g polymer-cli +git clone https://github.com/PolymerLabs/polymer-3-first-element.git +---- +a| +- https://polymer-library.polymer-project.org/3.0/docs/first-element/intro[Polymer] +| + +a| +[#qwik] +=== Qwik +|✓ +a| +[source,bash] +---- +npm create qwik@latest +---- +a| +- https://qwik.builder.io/docs/getting-started/[Qwik] +| + +a| +[#cycle] +=== Cycle +|✓ +a| +[source,bash] +---- +npm install --global create-cycle-app +create-cycle-app my-awesome-app +---- +a| +- https://cycle.js.org/getting-started.html[Cycle] +| + +a| +[#riotjs] +=== Riot.js +|✓ +a| +[source,bash] +---- +npm init riot@latest +---- +a| +- https://riot.js.org/[Riot.js] +| + +a| +[#refine] +=== Refine +|✓ +a| +[source,bash] +---- +npm create refine-app@latest +---- +a| +- https://refine.dev/docs/getting-started/quickstart/[Refine] +| + +a| +[#midwayjs] +=== Midway.js +|✓ +| No +a| +- https://www.midwayjs.org/[Midway.js] +| + +|=== + +== Required Configuration + +[#angular-test-config] +=== Angular Test Configuration + +If you want to use the Angular tests (instead of Playwright from the @QuarkusTest): + +.karma.conf.js: +[source,javascript] +---- + browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } +}, +---- + +[#nextjs-config] +=== Next.js Configuration + +This will configure the build to export the static files: +.package.json: +[source,json] +---- + "scripts": { + ... + "build": "next build && next export", + } +---- + +[#svelte-kit-config] +=== Svelte Kit Configuration + +It will work in production mode though with the following chnages: + +Install Svelte Static adapter: + +[source,shell] +---- +npm i -D @sveltejs/adapter-static +---- + +Configure `svelte.config` file with the following changes: + +[source,json] +---- +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + fallback: 'index.html' + }) + } +}; + +export default config; +---- + +In `application.properties` add: +[source,properties] +---- +quarkus.quinoa.dev-server=false +quarkus.quinoa.build-dir=build +---- diff --git a/docs/templates/includes/attributes.adoc b/docs/templates/includes/attributes.adoc index 16e6708b..121fc242 100644 --- a/docs/templates/includes/attributes.adoc +++ b/docs/templates/includes/attributes.adoc @@ -1,6 +1,7 @@ :quarkus-version: ${quarkus.version} :quarkus-quinoa-version: ${release.current-version} :maven-version: 3.8.1+ +:extension-status: stable :quarkus-org-url: https://github.com/quarkusio :quarkus-base-url: {quarkus-org-url}/quarkus diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index b604115b..9176923c 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -3,7 +3,7 @@ %root-path.quarkus.quinoa.enable-spa-routing=true %root-path.quarkus.quinoa.build-dir=dist %root-path.quarkus.quinoa.index-page=app.html -%root-path.quarkus.quinoa.package-manager-command.build=npm run build-per-env +%root-path.quarkus.quinoa.package-manager-command.build=run build-per-env %root-path.quarkus.quinoa.package-manager-command.build-env.FOO=bar %root-path.quarkus.quinoa.package-manager-command.build-env.ROOT_PATH=${quarkus.http.root-path}/ @@ -21,7 +21,7 @@ %lit.quarkus.quinoa.ui-dir=src/main/ui-lit %lit.quarkus.quinoa.build-dir=dist %lit.quarkus.quinoa.index-page=app.html -%lit.quarkus.quinoa.package-manager-command.build=npm run build-per-env +%lit.quarkus.quinoa.package-manager-command.build=run build-per-env %lit.quarkus.quinoa.package-manager-command.build-env.FOO=bar %lit.quarkus.quinoa.package-manager-command.build-env.ROOT_PATH=/ %yarn.quarkus.quinoa.package-manager=yarn diff --git a/integration-tests/src/main/ui-angular/package.json b/integration-tests/src/main/ui-angular/package.json index 7c0c4926..d82868f5 100644 --- a/integration-tests/src/main/ui-angular/package.json +++ b/integration-tests/src/main/ui-angular/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve --host 0.0.0.0 --disable-host-check", + "start": "ng serve", "build": "npm run something && ng build", "something": "echo \"something\"", "watch": "ng build --watch --configuration development", diff --git a/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUIAngularTest.java b/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUIAngularTest.java index 889af19e..b6db7ec1 100644 --- a/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUIAngularTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUIAngularTest.java @@ -5,7 +5,6 @@ import java.net.URL; -import io.quarkiverse.playwright.WithPlaywright; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -15,6 +14,7 @@ import com.microsoft.playwright.Response; import io.quarkiverse.playwright.InjectPlaywright; +import io.quarkiverse.playwright.WithPlaywright; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; diff --git a/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUILitTest.java b/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUILitTest.java index 07d2740a..64e8cc75 100644 --- a/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUILitTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/quinoa/it/QuinoaUILitTest.java @@ -2,7 +2,6 @@ import java.net.URL; -import io.quarkiverse.playwright.WithPlaywright; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -12,6 +11,7 @@ import com.microsoft.playwright.Response; import io.quarkiverse.playwright.InjectPlaywright; +import io.quarkiverse.playwright.WithPlaywright; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile;