Skip to content

Commit

Permalink
Improve Framework detection and tests (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
ia3andy authored Sep 27, 2023
1 parent b8e43a0 commit f373cc1
Show file tree
Hide file tree
Showing 22 changed files with 303 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ public ForwardedDevServerBuildItem prepareDevService(
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 resolvedHostAddress = PackageManagerRunner.isDevServerUp(configuredDevServerHost, port, checkPath);
if (resolvedHostAddress != null) {
return new ForwardedDevServerBuildItem(resolvedHostAddress, port);
final String resolvedHostIPAddress = PackageManagerRunner.isDevServerUp(configuredDevServerHost, port, checkPath);
if (resolvedHostIPAddress != null) {
return new ForwardedDevServerBuildItem(resolvedHostIPAddress, port);
} else {
throw new IllegalStateException(
"The Web UI dev server (configured as not managed by Quinoa) is not started on port: " + port);
Expand Down Expand Up @@ -153,7 +153,7 @@ public ForwardedDevServerBuildItem prepareDevService(
devService = new DevServicesResultBuildItem.RunningDevService(
DEV_SERVICE_NAME, null, onClose, devServerConfigMap);
devServices.produce(devService.toBuildItem());
return new ForwardedDevServerBuildItem(devServer.hostAddress(), port);
return new ForwardedDevServerBuildItem(devServer.hostIPAddress(), port);
} catch (Throwable t) {
packageManagerRunner.stopDev(dev.get());
if (devServer != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public class QuinoaProcessor {
private static final Logger LOG = Logger.getLogger(QuinoaProcessor.class);
private static final Set<String> IGNORE_WATCH = Set.of("node_modules", "target");
private static final Set<String> IGNORE_WATCH_BUILD_DIRS = Arrays.stream(FrameworkType.values()).sequential()
.map(frameworkType -> frameworkType.factory().getFrameworkBuildDir())
.map(frameworkType -> frameworkType.factory().getDefaultBuildDir())
.collect(Collectors.toSet());
private static final Pattern IGNORE_WATCH_REGEX = Pattern.compile("^[.].+$"); // ignore "." directories

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

public interface FrameworkConfigOverrideFactory {

String getFrameworkBuildDir();
String getDefaultBuildDir();

String getFrameworkDevScriptName();
String getDefaultDevScriptName();

QuinoaConfig override(QuinoaConfig delegate, Optional<JsonObject> packageJson);
QuinoaConfig override(QuinoaConfig delegate, Optional<JsonObject> packageJson, Optional<String> detectedDevScript,
boolean isCustomized);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeSet;
import java.util.stream.Collectors;

Expand All @@ -19,6 +22,7 @@
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.json.JsonString;
import jakarta.json.JsonValue.ValueType;

import org.jboss.logging.Logger;

Expand All @@ -33,31 +37,27 @@
*/
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)),
REACT(Set.of("react-scripts start", "react-app-rewired start", "craco start"), new ReactFramework()),
VUE_LEGACY(Set.of("vue-cli-service serve"), 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)),
SOLID_START(Set.of("solid-start dev"), generic("dist", "dev", 3000)),
ASTRO(Set.of("astro dev"), generic("dist", "dev", 3000)),
NEXT(Set.of("next dev"), new NextFramework()),
NUXT(Set.of("nuxt dev"), 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));
EMBER(Set.of("ember-cli serve"), generic("dist", "serve", 4200)),
GATSBY(Set.of("gatsby develop"), generic("dist", "develop", 8000)),
MIDWAYJS(Set.of("midway-bin dev"), generic("dist", "dev", 7001));

private static final Logger LOG = Logger.getLogger(FrameworkType.class);

public static final Set<String> DEV_SCRIPTS = Arrays.stream(values())
.map(framework -> framework.factory.getFrameworkDevScriptName())
private static final Set<String> DEV_SCRIPTS = Arrays.stream(values())
.map(framework -> framework.factory.getDefaultDevScriptName())
.collect(Collectors.toCollection(TreeSet::new));

private static final Map<String, FrameworkType> TYPE_START_DEV_COMMAND_MAPPING = Arrays.stream(values())
.flatMap(t -> t.cliStartDev.stream().map(c -> new SimpleImmutableEntry<>(c, t)))
.collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue));
private final Set<String> cliStartDev;
private final FrameworkConfigOverrideFactory factory;

Expand All @@ -72,53 +72,87 @@ public FrameworkConfigOverrideFactory factory() {

public static QuinoaConfig overrideConfig(LaunchModeBuildItem launchMode, QuinoaConfig config, Path packageJsonFile) {
if (!config.framework().detection()) {
return UNKNOWN_FRAMEWORK.override(config, Optional.empty());
return UNKNOWN_FRAMEWORK.override(config, Optional.empty(), Optional.empty(), true);
}

JsonObject packageJson = null;
final JsonObject packageJson = readPackageJson(packageJsonFile);
final Optional<DetectedFramework> detectedFramework = detectFramework(packageJson);

if (detectedFramework.isEmpty()) {
LOG.trace("Quinoa could not auto-detect the frameworkType from package.json file.");
return UNKNOWN_FRAMEWORK.override(config, Optional.of(packageJson), Optional.empty(), true);
}

final FrameworkType frameworkType = detectedFramework.get().type;
LOG.infof("Quinoa detected '%s' frameworkType from package.json file.", frameworkType);

return frameworkType.factory.override(config, Optional.of(packageJson), Optional.of(detectedFramework.get().devScript),
detectedFramework.get().isCustomized);
}

private static JsonObject readPackageJson(Path packageJsonFile) {
try (JsonReader reader = Json.createReader(Files.newInputStream(packageJsonFile))) {
packageJson = reader.readObject();
return reader.readObject();
} catch (IOException | JsonException e) {
LOG.warnf("Quinoa failed to read the package.json file. %s", e.getMessage());
throw new RuntimeException("Quinoa failed to read the package.json file. %s", e);
}
}

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;
}
static Optional<DetectedFramework> detectFramework(JsonObject packageJson) {
final JsonObject scripts = packageJson.getJsonObject("scripts");
if (scripts == null || scripts.isEmpty()) {
return Optional.empty();
}
final Map<String, String> projectDevScripts = scripts.entrySet().stream()
.filter(e -> DEV_SCRIPTS.contains(e.getKey()) && e.getValue() != null
&& ValueType.STRING.equals(e.getValue().getValueType()))
.collect(Collectors.toMap(Map.Entry::getKey,
e -> ((JsonString) e.getValue()).getString().trim().toLowerCase(Locale.US)));

for (Map.Entry<String, String> e : projectDevScripts.entrySet()) {
if (TYPE_START_DEV_COMMAND_MAPPING.containsKey(e.getValue())) {
final FrameworkType framework = TYPE_START_DEV_COMMAND_MAPPING.get(e.getValue());
final String frameworkDefaultDevScript = framework.factory().getDefaultDevScriptName();
LOG.debugf("Detected framework with dev command perfect match: %s", framework.toString());
final String projectDevScript = e.getKey();
final boolean isDefaultDevCommand = projectDevScript.equals(frameworkDefaultDevScript);
if (!isDefaultDevCommand) {
LOG.warnf("%s framework typically defines a '%s` script in package.json file but found '%s' instead.",
framework, frameworkDefaultDevScript, projectDevScript);
}
return Optional.of(new DetectedFramework(framework, projectDevScript, !isDefaultDevCommand));
}
}

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);
// Fallback to partial match
for (Map.Entry<String, String> projectDevScriptEntry : projectDevScripts.entrySet()) {
for (Map.Entry<String, FrameworkType> frameworksByCommand : TYPE_START_DEV_COMMAND_MAPPING.entrySet()) {
final FrameworkType framework = frameworksByCommand.getValue();
final String frameworkDefaultDevScript = framework.factory().getDefaultDevScriptName();
final String frameworkCommand = frameworksByCommand.getKey();
final String projectDevScript = projectDevScriptEntry.getKey();
final String projectDevCommand = projectDevScriptEntry.getValue();
final boolean detected = projectDevCommand.contains(frameworkCommand);
if (detected) {
final boolean isDefaultDevCommand = projectDevScript.equals(frameworkDefaultDevScript);
final boolean endsWith = projectDevCommand.endsWith(frameworkCommand);
if (isDefaultDevCommand || projectDevScripts.size() == 1) {
// We can assume this is the right dev script
if (!isDefaultDevCommand) {
LOG.warnf(
"%s framework typically defines a '%s` script in package.json file but found '%s' instead.",
framework, frameworkDefaultDevScript, projectDevScript);
}
return Optional.of(new DetectedFramework(framework, projectDevScript,
!isDefaultDevCommand || !endsWith));
} else {
LOG.errorf(
"Framework detection failed: It is probably %s framework which typically defines a '%s' script in package.json but found multiple other dev scripts instead (%s).",
framework, frameworkDefaultDevScript, String.join(", ", projectDevScripts.keySet()));
}
}
}
}

LOG.infof("Quinoa detected '%s' framework from package.json file.", frameworkType);
return frameworkType.factory.override(config, Optional.ofNullable(packageJson));
return Optional.empty();
}

/**
Expand All @@ -128,15 +162,52 @@ public static QuinoaConfig overrideConfig(LaunchModeBuildItem launchMode, Quinoa
* @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);
final String normalizedScript = script.toLowerCase(Locale.ROOT).trim();
for (FrameworkType value : values()) {
for (String cliName : value.cliStartDev) {
if (lowerScript.contains(cliName)) {
if (normalizedScript.contains(cliName)) {
return value;
}
}
}
return null;
}

static class DetectedFramework {
public final FrameworkType type;

public final String devScript;
public final boolean isCustomized;

public DetectedFramework(FrameworkType type, String devScript, boolean isCustomized) {
this.type = type;
this.devScript = devScript;
this.isCustomized = isCustomized;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
DetectedFramework that = (DetectedFramework) o;
return isCustomized == that.isCustomized && type == that.type && Objects.equals(devScript, that.devScript);
}

@Override
public int hashCode() {
return Objects.hash(type, devScript, isCustomized);
}

@Override
public String toString() {
return new StringJoiner(", ", DetectedFramework.class.getSimpleName() + "[", "]")
.add("type=" + type)
.add("devScript='" + devScript + "'")
.add("isCustomized=" + isCustomized)
.toString();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,33 @@ public AngularFramework() {
}

@Override
public QuinoaConfig override(QuinoaConfig delegate, Optional<JsonObject> packageJson) {
return new QuinoaConfigDelegate(super.override(delegate, packageJson)) {
public QuinoaConfig override(QuinoaConfig originalConfig, Optional<JsonObject> packageJson,
Optional<String> detectedDevScript, boolean isCustomized) {
final String devScript = detectedDevScript.orElse(getDefaultDevScriptName());
return new QuinoaConfigDelegate(super.override(originalConfig, packageJson, detectedDevScript, isCustomized)) {
@Override
public Optional<String> 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));
final String fullBuildDir = String.format("%s/%s", getDefaultBuildDir(), applicationName);
return Optional.of(originalConfig.buildDir().orElse(fullBuildDir));
}

@Override
public PackageManagerCommandConfig packageManagerCommand() {
return new PackageManagerCommandConfigDelegate(super.packageManagerCommand()) {
@Override
public Optional<String> dev() {
return Optional.of(delegate.packageManagerCommand().dev()
.orElse("run " + getFrameworkDevScriptName() + " -- --disable-host-check"));
final String extraArgs = isCustomized ? "" : " -- --disable-host-check --hmr";
return Optional.of(originalConfig.packageManagerCommand().dev()
.orElse("run " + devScript + extraArgs));
}

@Override
public Optional<String> test() {
return Optional.of(delegate.packageManagerCommand().test()
.orElse(DEFAULT_BUILD_COMMAND + " -- --no-watch --no-progress --browsers=ChromeHeadlessCI"));
final String extraArgs = isCustomized ? "" : " -- --no-watch --no-progress --browsers=ChromeHeadlessCI";
return Optional.of(originalConfig.packageManagerCommand().test()
.orElse(DEFAULT_TEST_COMMAND + extraArgs));
}
};
}
Expand Down
Loading

0 comments on commit f373cc1

Please sign in to comment.