Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking Change] Fix #526: Detect package.json changes for install #527

Merged
merged 3 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import io.quarkus.deployment.util.FileUtil;
import io.quarkus.dev.console.QuarkusConsole;
Expand All @@ -21,7 +22,7 @@ public class QuinoaQuarkusUnitTest {
private static final String CI = System.getProperty("CI");

private final Path testDir;
private boolean nodeModules = false;
private boolean alreadyInstalled = false;
private String initialLockfile = "package-lock.json";
private Boolean ci = false;

Expand All @@ -42,8 +43,8 @@ public QuinoaQuarkusUnitTest initialLockfile(String lockFile) {
return this;
}

public QuinoaQuarkusUnitTest nodeModules() {
this.nodeModules = true;
public QuinoaQuarkusUnitTest alreadyInstalled() {
this.alreadyInstalled = true;
return this;
}

Expand All @@ -63,7 +64,7 @@ public QuarkusUnitTest toQuarkusUnitTest() {
.setBeforeAllCustomizer(new Runnable() {
@Override
public void run() {
prepareTestWebUI(testDir, nodeModules);
prepareTestWebUI(testDir, alreadyInstalled);
prepareLockFile(testDir, initialLockfile);
if (ci != null) {
System.setProperty("CI", ci.toString());
Expand Down Expand Up @@ -92,14 +93,18 @@ public static boolean isWindows() {
return QuarkusConsole.IS_WINDOWS;
}

public static void prepareTestWebUI(Path testDir, boolean nodeModules) {
public static void prepareTestWebUI(Path testDir, boolean installed) {
final Path webUI = Path.of("src/test/webui/");
try {
FileUtil.deleteDirectory(testDir);
Files.createDirectories(testDir);
copyDirectory(webUI, testDir);
if (nodeModules) {
if (installed) {
Files.createDirectory(testDir.resolve("node_modules"));
final Path targetQuinoaDir = Path.of("target/quinoa/");
Files.createDirectories(targetQuinoaDir);
Files.copy(testDir.resolve("package.json"), targetQuinoaDir.resolve("package.json"),
StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new IllegalStateException("Error while preparing the test web ui directory.", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public ForwardedDevServerBuildItem prepareDevService(
final PackageManagerRunner packageManagerRunner = installedPackageManager.getPackageManager();
final String checkPath = resolvedConfig.devServer().checkPath().orElse(null);
if (devService != null) {
boolean shouldShutdownTheBroker = !resolvedConfig.equals(oldConfig);
boolean shouldShutdownTheBroker = !resolvedConfig.equals(oldConfig)
|| QuinoaProcessor.isPackageJsonLiveReloadChanged(configuredQuinoa, liveReload);
if (!shouldShutdownTheBroker) {
if (devServerConfig.port().isEmpty()) {
throw new IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
Expand All @@ -38,7 +39,6 @@
import io.quarkiverse.quinoa.deployment.items.TargetDirBuildItem;
import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerInstall;
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;
Expand Down Expand Up @@ -68,7 +68,9 @@ public class QuinoaProcessor {
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";
private static final String TARGET_DIR_NAME = "quinoa";
private static final String TARGET_BUILD_DIR_NAME = "build";
private static final String BUILD_FILE = "package.json";

@BuildStep
FeatureBuildItem feature() {
Expand All @@ -79,7 +81,7 @@ FeatureBuildItem feature() {
public ConfiguredQuinoaBuildItem prepareQuinoaDirectory(
LaunchModeBuildItem launchMode,
QuinoaConfig userConfig,
OutputTargetBuildItem outputTarget) {
OutputTargetBuildItem outputTarget) throws IOException {
if (!isEnabled(userConfig)) {
LOG.info("Quinoa is disabled.");
return null;
Expand All @@ -94,11 +96,13 @@ public ConfiguredQuinoaBuildItem prepareQuinoaDirectory(
if (projectDirs == null) {
return null;
}
final Path packageJson = projectDirs.uiDir.resolve("package.json");
final Path packageJson = projectDirs.uiDir.resolve(BUILD_FILE);
if (!Files.isRegularFile(packageJson)) {
throw new ConfigurationException("No package.json found in Web UI directory: '" + configuredDir + "'");
throw new ConfigurationException("No " + BUILD_FILE + " found in Web UI directory: '" + configuredDir + "'");
}

initializeTargetDirectory(outputTarget);

final QuinoaConfig resolvedConfig = overrideConfig(launchMode, userConfig, packageJson);

return new ConfiguredQuinoaBuildItem(projectDirs.projectRootDir, projectDirs.uiDir, packageJson, resolvedConfig);
Expand All @@ -107,7 +111,8 @@ public ConfiguredQuinoaBuildItem prepareQuinoaDirectory(
@BuildStep
public InstalledPackageManagerBuildItem install(
ConfiguredQuinoaBuildItem configuredQuinoa,
LiveReloadBuildItem liveReload) {
LiveReloadBuildItem liveReload,
OutputTargetBuildItem outputTarget) throws IOException {
if (configuredQuinoa != null) {
final QuinoaConfig resolvedConfig = configuredQuinoa.resolvedConfig();
Optional<String> packageManagerBinary = resolvedConfig.packageManager();
Expand All @@ -122,18 +127,18 @@ public InstalledPackageManagerBuildItem install(

final PackageManagerRunner packageManagerRunner = autoDetectPackageManager(packageManagerBinary,
resolvedConfig.packageManagerCommand(), configuredQuinoa.uiDir(), paths);
final boolean alreadyInstalled = Files.isDirectory(packageManagerRunner.getDirectory().resolve("node_modules"));
final boolean packageFileModified = liveReload.isLiveReload()
&& liveReload.getChangedResources().stream()
.anyMatch(r -> r.equals(configuredQuinoa.packageJson().toString()));
if (resolvedConfig.forceInstall() || !alreadyInstalled || packageFileModified) {
final Path targetPackageJson = outputTarget.getOutputDirectory().resolve(TARGET_DIR_NAME).resolve(BUILD_FILE);
final Path currentPackageJson = configuredQuinoa.packageJson();
if (resolvedConfig.forceInstall()
|| shouldInstallPackages(configuredQuinoa, liveReload, targetPackageJson, currentPackageJson)) {
final boolean ci = resolvedConfig.ci().orElseGet(QuinoaProcessor::isCI);
if (ci) {
packageManagerRunner.ci();
} else {
packageManagerRunner.install();
}

// copy the package.json to build, so we can compare for next time
Files.copy(currentPackageJson, targetPackageJson, StandardCopyOption.REPLACE_EXISTING);
}
return new InstalledPackageManagerBuildItem(packageManagerRunner);
}
Expand Down Expand Up @@ -173,7 +178,8 @@ && isDevServerMode(configuredQuinoa.resolvedConfig())) {
throw new ConfigurationException("Quinoa build directory not found: '" + buildDir.toAbsolutePath() + "'",
Set.of("quarkus.quinoa.build-dir"));
}
final Path targetBuildDir = outputTarget.getOutputDirectory().resolve(TARGET_DIR_NAME);

final Path targetBuildDir = initializeTargetDirectory(outputTarget).resolve(TARGET_BUILD_DIR_NAME);
FileUtil.deleteDirectory(targetBuildDir);
try {
Files.move(buildDir, targetBuildDir);
Expand Down Expand Up @@ -211,11 +217,19 @@ public BuiltResourcesBuildItem prepareResourcesForOtherMode(
return new BuiltResourcesBuildItem(targetDir.get().getBuildDirectory(), entries);
}

@BuildStep
@BuildStep(onlyIf = IsDevelopment.class)
void watchChanges(
Optional<ConfiguredQuinoaBuildItem> quinoaDir,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedPaths) throws IOException {
if (quinoaDir.isEmpty() || isDevServerMode(quinoaDir.get().resolvedConfig())) {
if (quinoaDir.isEmpty()) {
return;
}
if (isDevServerMode(quinoaDir.get().resolvedConfig())) {
final HotDeploymentWatchedFileBuildItem watchPackageJson = HotDeploymentWatchedFileBuildItem.builder()
.setLocation(quinoaDir.get().packageJson().toString())
.setRestartNeeded(true)
.build();
watchedPaths.produce(watchPackageJson);
return;
}
scan(quinoaDir.get().uiDir(), watchedPaths);
Expand Down Expand Up @@ -256,21 +270,6 @@ public void runtimeInit(
}
}

@BuildStep(onlyIf = IsDevelopment.class)
List<HotDeploymentWatchedFileBuildItem> hotDeploymentWatchedFiles(Optional<ConfiguredQuinoaBuildItem> configuredQuinoa,
OutputTargetBuildItem outputTarget) {
final List<HotDeploymentWatchedFileBuildItem> watchedFiles = new ArrayList<>(PackageManagerType.values().length);
if (configuredQuinoa.isEmpty()) {
return watchedFiles;
}

for (PackageManagerType pm : PackageManagerType.values()) {
final String watchFile = configuredQuinoa.get().uiDir().resolve(pm.getLockFile()).toString();
watchedFiles.add(new HotDeploymentWatchedFileBuildItem(watchFile));
}
return watchedFiles;
}

private HashSet<BuiltResourcesBuildItem.BuiltResource> prepareBuiltResources(
BuildProducer<GeneratedResourceBuildItem> generatedResources,
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
Expand Down Expand Up @@ -309,6 +308,41 @@ private void scan(Path directory, BuildProducer<HotDeploymentWatchedFileBuildIte
}
}

private static boolean shouldInstallPackages(ConfiguredQuinoaBuildItem configuredQuinoa,
LiveReloadBuildItem liveReload,
Path targetPackageJson,
Path currentPackageJson) throws IOException {

if (!Files.isDirectory(configuredQuinoa.uiDir().resolve("node_modules"))) {
LOG.info("Quinoa didn't detect a node_modules directory, let's install packages...");
return true;
}

if (isPackageJsonLiveReloadChanged(configuredQuinoa, liveReload)) {
return true;
}

if (!Files.exists(targetPackageJson)) {
LOG.info("Fresh Quinoa build, let's install packages...");
return true;
}
// Check for size then content
if (Files.size(currentPackageJson) != Files.size(targetPackageJson)
|| !Arrays.equals(Files.readAllBytes(currentPackageJson), Files.readAllBytes(targetPackageJson))) {
LOG.info("Quinoa detected a change in package.json since the previous install, let's install packages again...");
return true;
}

LOG.debug("package.json seems to be the same as previous Quinoa install, skipping packages install");
return false;
}

static boolean isPackageJsonLiveReloadChanged(ConfiguredQuinoaBuildItem configuredQuinoa, LiveReloadBuildItem liveReload) {
return liveReload.isLiveReload()
&& liveReload.getChangedResources().stream()
.anyMatch(r -> r.equals(configuredQuinoa.packageJson().toString()));
}

/**
* 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
Expand Down Expand Up @@ -382,6 +416,12 @@ private static boolean isCI() {
return Objects.equals(ci, "true");
}

public static Path initializeTargetDirectory(OutputTargetBuildItem outputTarget) throws IOException {
final Path targetBuildDir = outputTarget.getOutputDirectory().resolve(TARGET_DIR_NAME);
Files.createDirectories(targetBuildDir);
return targetBuildDir;
}

private static class QuinoaLiveContext {
private final Path location;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.quarkiverse.quinoa.deployment.config;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.smallrye.config.WithDefault;

@ConfigGroup
public interface FrameworkConfig {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public interface QuinoaConfig {
* 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).
* Quinoa put the built files in 'target/quinoa/build' (or 'build/quinoa/build with Gradle).
*/
@WithDefault("false")
boolean justBuild();
Expand All @@ -55,7 +55,7 @@ public interface QuinoaConfig {
/**
* 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.
* 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 + "'")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
package io.quarkiverse.quinoa.deployment.devui;

import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

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.items.InstalledPackageManagerBuildItem;
import io.quarkiverse.quinoa.deployment.packagemanager.PackageManagerRunner;
import io.quarkiverse.quinoa.devui.QuinoaJsonRpcService;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.dev.console.DevConsoleManager;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.ExternalPageBuilder;
import io.quarkus.devui.spi.page.FooterPageBuildItem;
Expand Down Expand Up @@ -100,27 +93,4 @@ void createCard(BuildProducer<CardPageBuildItem> cardPageBuildItemBuildProducer,
footerProducer.produce(new FooterPageBuildItem(nodeLogPageBuilder));
}

@BuildStep(onlyIf = IsDevelopment.class)
JsonRPCProvidersBuildItem registerJsonRpcBackend(InstalledPackageManagerBuildItem installedPackageManager,
QuinoaConfig quinoaConfig) {
DevConsoleManager.register("quinoa-install-action",
install(installedPackageManager, quinoaConfig));
return new JsonRPCProvidersBuildItem(QuinoaJsonRpcService.class);
}

private Function<Map<String, String>, String> install(InstalledPackageManagerBuildItem installedPackageManager,
QuinoaConfig quinoaConfig) {
return (map -> {
try {
final PackageManagerRunner packageManagerRunner = installedPackageManager.getPackageManager();

// install or update packages
packageManagerRunner.install();

return "installed";
} catch (Exception e) {
return e.getMessage();
}
});
}
}
Loading