Skip to content

Commit

Permalink
Fix #367: Framework automation detection
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware committed Jun 2, 2023
1 parent d12089a commit e38544d
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@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";

Expand Down Expand Up @@ -60,7 +61,7 @@ public class QuinoaConfig {
* The path is relative to the Web UI path.
* If not set "build/" will be used
*/
@ConfigItem(defaultValue = "build/")
@ConfigItem(defaultValue = DEFAULT_BUILD_DIR)
public String buildDir;

/**
Expand Down Expand Up @@ -188,4 +189,4 @@ public int hashCode() {
return Objects.hash(enable, uiDir, buildDir, packageManager, packageManagerInstall, packageManagerCommand, indexPage,
runTests, frozenLockfile, forceInstall, enableSPARouting, ignoredPathPrefixes, devServer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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.QuinoaHandlerConfig;
import io.quarkiverse.quinoa.QuinoaRecorder;
import io.quarkiverse.quinoa.deployment.packagemanager.FrameworkType;
import io.quarkiverse.quinoa.deployment.packagemanager.PackageManager;
import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstall;
import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerType;
Expand Down Expand Up @@ -84,10 +91,9 @@ public QuinoaDirectoryBuildItem prepareQuinoaDirectory(
if (projectDirs == null) {
return null;
}
final Path packageFile = projectDirs.uiDir.resolve("package.json");
if (!Files.isRegularFile(packageFile)) {
throw new ConfigurationException(
"No package.json found in Web UI directory: '" + configuredDir + "'");
final Path packageJsonFile = projectDirs.uiDir.resolve("package.json");
if (!Files.isRegularFile(packageJsonFile)) {
throw new ConfigurationException("No package.json found in Web UI directory: '" + configuredDir + "'");
}
Optional<String> packageManagerBinary = quinoaConfig.packageManager;
List<String> paths = new ArrayList<>();
Expand All @@ -101,11 +107,15 @@ public QuinoaDirectoryBuildItem prepareQuinoaDirectory(
quinoaConfig.packageManagerCommand, projectDirs.getUIDir(), paths);
final boolean alreadyInstalled = Files.isDirectory(packageManager.getDirectory().resolve("node_modules"));
final boolean packageFileModified = liveReload.isLiveReload()
&& liveReload.getChangedResources().stream().anyMatch(r -> r.equals(packageFile.toString()));
&& 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);
}

// attempt to autoconfigure settings based on the framework being used
autoDetectFramework(packageJsonFile, quinoaConfig, launchMode);

return new QuinoaDirectoryBuildItem(packageManager);
}

Expand Down Expand Up @@ -237,6 +247,53 @@ List<HotDeploymentWatchedFileBuildItem> hotDeploymentWatchedFiles(QuinoaConfig q
return watchedFiles;
}

private void autoDetectFramework(Path packageJsonFile, QuinoaConfig quinoaConfig, LaunchModeBuildItem launchMode) {
JsonString applicationName = null;
JsonString startScript = null;
try (JsonReader reader = Json.createReader(Files.newInputStream(packageJsonFile))) {
JsonObject root = reader.readObject();
applicationName = root.getJsonString("name");
JsonObject scripts = root.getJsonObject("scripts");
if (scripts != null) {
startScript = scripts.getJsonString("start");
if (startScript == null) {
startScript = scripts.getJsonString("dev");
}
}
} catch (IOException e) {
LOG.warnf("Quinoa failed to auto-detect the framework from package.json file. %s", e.getMessage());
}

if (startScript == null) {
LOG.info("Quinoa could not auto-detect the framework from package.json file.");
return;
}

// check if we found a script to detect which framework
final FrameworkType frameworkType = FrameworkType.evaluate(startScript.getString());
if (frameworkType == null) {
LOG.info("Quinoa could not auto-detect the framework from package.json file.");
return;
}

LOG.infof("%s framework automatically detected from package.json file.", frameworkType);
// only override properties that have not been set
if (launchMode.getLaunchMode() != LaunchMode.NORMAL && quinoaConfig.devServer.port.isEmpty()) {
LOG.infof("%s framework setting dev server port: %d", frameworkType, frameworkType.getDevServerPort());
quinoaConfig.devServer.port = OptionalInt.of(frameworkType.getDevServerPort());
}
if (QuinoaConfig.DEFAULT_BUILD_DIR.equalsIgnoreCase(quinoaConfig.buildDir)) {
String newDirectory = frameworkType.getBuildDirectory();

// Angular builds a custom directory "dist/[appname]"
if (frameworkType == FrameworkType.ANGULAR) {
newDirectory = String.format(newDirectory, applicationName);
}
LOG.infof("%s framework setting build directory: '%s'", frameworkType, newDirectory);
quinoaConfig.buildDir = newDirectory;
}
}

private HashSet<BuiltResourcesBuildItem.BuiltResource> prepareBuiltResources(
BuildProducer<GeneratedResourceBuildItem> generatedResources,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
Expand Down Expand Up @@ -275,7 +332,7 @@ private void scan(Path directory, BuildProducer<HotDeploymentWatchedFileBuildIte
}
}

private ProjectDirs resolveProjectDirs(QuinoaConfig config,
private static ProjectDirs resolveProjectDirs(QuinoaConfig config,
OutputTargetBuildItem outputTarget) {
Path projectRoot = findProjectRoot(outputTarget.getOutputDirectory());
Path configuredUIDirPath = Path.of(config.uiDir.trim());
Expand Down Expand Up @@ -354,4 +411,4 @@ public Path getUIDir() {
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.quarkiverse.quinoa.deployment.packagemanager;

import java.util.Locale;
import java.util.Set;

/**
* 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")),
VITE("dist", "dev", 5173, Set.of("vite")),
NEXT("out", "dev", 3000, Set.of("next")),
ANGULAR("dist/%s", "start", 4200, Set.of("ng")),
WEB_COMPONENTS("dist", "start", 8003, Set.of("web-dev-server"));

/**
* 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.
*/
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<String> packageScripts;

FrameworkType(String buildDirectory, String devScript, int devServerPort, Set<String> packageScripts) {
this.buildDirectory = buildDirectory;
this.devScript = devScript;
this.devServerPort = devServerPort;
this.packageScripts = packageScripts;
}

/**
* 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
*/
public static FrameworkType evaluate(String script) {
final String lowerScript = script.toLowerCase(Locale.ROOT);
for (FrameworkType value : values()) {
Set<String> 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<String> getPackageScripts() {
return packageScripts;
}

public int getDevServerPort() {
return devServerPort;
}
}
4 changes: 2 additions & 2 deletions docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:quarkus-version: 3.0.0.CR2
:quarkus-quinoa-version: 2.0.2
:quarkus-version: 3.1.0.Final
:quarkus-quinoa-version: 2.0.4
:maven-version: 3.8.1+

:quarkus-org-url: https://github.com/quarkusio
Expand Down
28 changes: 5 additions & 23 deletions docs/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,8 @@ NOTE: Quinoa relies on the dev server returning a 404 when the file is not found
[#react]
=== React

App created by Create React App (https://create-react-app.dev/docs/getting-started) are compatible without any change.
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.

To enable React live coding server:
[source,properties]
----
quarkus.quinoa.dev-server.port=3000
----

[#angular]
=== Angular
Expand All @@ -258,11 +253,7 @@ App created by `ng` (https://angular.io/guide/setup-local) require a tiny bit of
quarkus.quinoa.build-dir=dist/[your-app-name]
----

To enable Angular live coding server, you need to edit the package.json start script with `ng serve --host 0.0.0.0 --disable-host-check`, then add this configuration:
[source,properties]
----
quarkus.quinoa.dev-server.port=4200
----
To enable Angular live coding server, you need to edit the package.json start script with `ng serve --host 0.0.0.0 --disable-host-check`.

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

Expand All @@ -289,13 +280,12 @@ Edit the karma.conf.js:

[#nextjs]
=== Next.js
Any app created with Next.js (https://nextjs.org/) should work with Quinoa after the following changes:
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=/
quarkus.quinoa.build-dir=out
----

In Dev mode Next.js serves everything out of root "/" but in PRD mode its the normal "/index.html".
Expand All @@ -312,20 +302,12 @@ Add these scripts to package.json

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

In application.properties add:
[source,properties]
----
quarkus.quinoa.dev-server.port=5173
quarkus.quinoa.build-dir=dist
----
Any app created with https://vitejs.dev/guide/[Vite] should work with Quinoa after the following changes:

Add start script to package.json
[source,json]
----
"scripts": {
...
"start": "vite"
},
----
Expand Down Expand Up @@ -496,4 +478,4 @@ On compatible CIs, don't forget to enable the Maven/Gradle and NPM/Yarn reposito
[[extension-configuration-reference]]
== Extension Configuration Reference

include::includes/quarkus-quinoa.adoc[leveloffset=+1, opts=optional]
include::includes/quarkus-quinoa.adoc[leveloffset=+1, opts=optional]

0 comments on commit e38544d

Please sign in to comment.