Skip to content

Commit

Permalink
feat: Store and use package-lock for bundle (#16782)
Browse files Browse the repository at this point in the history
* feat: Store and use package-lock for bundle

When making a bundle build if
no package-lock file exists in
project, use the dev bundle package-lock
that is stored when a dev bundle is built.

Acceptance order of lock files is:
project lock, src/dev-bundle, jar bundle.

Fixes #16041

* Rename method to mention bundle

Use constant in file name

* Also handle pnpm package-lock.yaml

* Log which lock file was not copied.
  • Loading branch information
caalador committed May 16, 2023
1 parent 8879166 commit 1b668e5
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 2 deletions.
Expand Up @@ -73,6 +73,11 @@ public final class Constants implements Serializable {

public static final String PACKAGE_LOCK_JSON = "package-lock.json";

/**
* Name of the <code>pnpm</code> version locking ile.
*/
public static final String PACKAGE_LOCK_YAML = "pnpm-lock.yaml";

/**
* Target folder constant.
*/
Expand Down
Expand Up @@ -15,23 +15,27 @@
*/
package com.vaadin.flow.server.frontend;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.internal.StringUtil;
import com.vaadin.flow.server.Constants;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH;

public final class BundleUtils {

Expand Down Expand Up @@ -128,4 +132,56 @@ public static boolean isPreCompiledProductionBundle() {
private static Logger getLogger() {
return LoggerFactory.getLogger(BundleUtils.class);
}

/**
* Copy package-lock.json/.yaml file from existing dev-bundle for building
* new bundle.
*
* @param options
* task options
*/
public static void copyPackageLockFromBundle(Options options) {
String lockFile;
if (options.isEnablePnpm()) {
lockFile = Constants.PACKAGE_LOCK_YAML;
} else {
lockFile = Constants.PACKAGE_LOCK_JSON;
}
File packageLock = new File(options.getNpmFolder(), lockFile);
if (packageLock.exists()) {
// NO-OP due to existing package-lock
return;
}

try {
copyAppropriatePackageLock(options, packageLock);
} catch (IOException ioe) {
getLogger().error(
"Failed to copy existing `" + lockFile + "` to use", ioe);
}

}

private static void copyAppropriatePackageLock(Options options,
File packageLock) throws IOException {
File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
String packageLockFile = options.isEnablePnpm()
? Constants.PACKAGE_LOCK_YAML
: Constants.PACKAGE_LOCK_JSON;
if (devBundleFolder.exists()) {
File devPackageLock = new File(devBundleFolder, packageLockFile);
if (devPackageLock.exists()) {
FileUtils.copyFile(devPackageLock, packageLock);
return;
}
}
final URL resource = options.getClassFinder()
.getResource(DEV_BUNDLE_JAR_PATH + packageLockFile);
if (resource != null) {
FileUtils.write(packageLock,
IOUtils.toString(resource, StandardCharsets.UTF_8),
StandardCharsets.UTF_8);
}
}
}
Expand Up @@ -117,6 +117,8 @@ public NodeTasks(Options options) {
options.withBundleBuild(needBuild);
if (!needBuild) {
commands.add(new TaskPrepareProdBundle(options));
} else {
BundleUtils.copyPackageLockFromBundle(options);
}
} else if (options.isBundleBuild()) {
// The dev bundle check needs the frontendDependencies to be
Expand All @@ -129,6 +131,7 @@ public NodeTasks(Options options) {
Mode.DEVELOPMENT_BUNDLE)) {
options.withRunNpmInstall(true);
options.withCopyTemplates(true);
BundleUtils.copyPackageLockFromBundle(options);
UsageStatistics.markAsUsed("flow/app-dev-bundle", null);
} else {
// A dev bundle build is not needed after all, skip it
Expand Down
Expand Up @@ -33,8 +33,6 @@

import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.shared.util.SharedUtil;

/**
Expand Down Expand Up @@ -101,6 +99,8 @@ public void execute() throws ExecutionFailedException {
runFrontendBuildTool("Vite", "vite/bin/vite.js", Collections.emptyMap(),
"build");

copyPackageLockToBundleFolder();

addReadme();
}

Expand Down Expand Up @@ -204,6 +204,28 @@ private void runFrontendBuildTool(String toolName, String executable,
}
}

private void copyPackageLockToBundleFolder() {
File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
assert devBundleFolder.exists() : "No dev-bundle folder created";

String packageLockFile = options.isEnablePnpm()
? Constants.PACKAGE_LOCK_YAML
: Constants.PACKAGE_LOCK_JSON;

File packageLockJson = new File(options.getNpmFolder(),
packageLockFile);
if (packageLockJson.exists()) {
try {
FileUtils.copyFile(packageLockJson,
new File(devBundleFolder, packageLockFile));
} catch (IOException e) {
getLogger().error("Failed to copy '" + packageLockFile + "' to "
+ Constants.DEV_BUNDLE_LOCATION, e);
}
}
}

private void addReadme() {
File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
Expand Down
@@ -1,23 +1,37 @@
package com.vaadin.flow.server.frontend;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import static com.vaadin.flow.server.Constants.DEV_BUNDLE_JAR_PATH;

public class BundleUtilsTest {

private List<AutoCloseable> closeOnTearDown = new ArrayList<>();

@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

@After
public void tearDown() {
for (AutoCloseable closeable : closeOnTearDown) {
Expand Down Expand Up @@ -88,4 +102,163 @@ private void mockStatsJsonLoading(JsonObject statsJson) {

}

@Test
public void packageLockExists_nothingIsCopied() throws IOException {
Options options = new Options(Mockito.mock(Lookup.class),
temporaryFolder.getRoot()).withBuildDirectory("target");

File packageLockFile = temporaryFolder
.newFile(Constants.PACKAGE_LOCK_JSON);
File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
devBundleFolder.mkdirs();
File devPackageLockJson = new File(devBundleFolder,
Constants.PACKAGE_LOCK_JSON);

final String existingLockFile = "{ \"existing\" }";
FileUtils.write(packageLockFile, existingLockFile);

FileUtils.write(devPackageLockJson, "{ \"bundleFile\"}");

BundleUtils.copyPackageLockFromBundle(options);

final String packageLockContents = FileUtils
.readFileToString(packageLockFile, StandardCharsets.UTF_8);

Assert.assertEquals("Existing file should not be overwritten",
existingLockFile, packageLockContents);
}

@Test
public void noPackageLockExists_devBundleLockIsCopied_notJarLock()
throws IOException {
final Lookup lookup = Mockito.mock(Lookup.class);
ClassFinder finder = Mockito.mock(ClassFinder.class);
Mockito.when(lookup.lookup(ClassFinder.class)).thenReturn(finder);

Options options = new Options(lookup, temporaryFolder.getRoot())
.withBuildDirectory("target");

File jarPackageLock = new File(options.getNpmFolder(), "temp.json");
final String jarPackageLockContent = "{ \"jarData\"}";
FileUtils.write(jarPackageLock, jarPackageLockContent);

Mockito.when(finder
.getResource(DEV_BUNDLE_JAR_PATH + Constants.PACKAGE_LOCK_JSON))
.thenReturn(jarPackageLock.toURI().toURL());

File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
devBundleFolder.mkdirs();
File devPackageLockJson = new File(devBundleFolder,
Constants.PACKAGE_LOCK_JSON);

final String packageLockContent = "{ \"bundleFile\"}";
FileUtils.write(devPackageLockJson, packageLockContent);

BundleUtils.copyPackageLockFromBundle(options);

final String packageLockContents = FileUtils.readFileToString(
new File(options.getNpmFolder(), Constants.PACKAGE_LOCK_JSON),
StandardCharsets.UTF_8);

Assert.assertEquals("dev-bundle file should be used",
packageLockContent, packageLockContents);
}

@Test
public void noPackageLockExists_jarDevBundleLockIsCopied()
throws IOException {
final Lookup lookup = Mockito.mock(Lookup.class);
ClassFinder finder = Mockito.mock(ClassFinder.class);
Mockito.when(lookup.lookup(ClassFinder.class)).thenReturn(finder);

Options options = new Options(lookup, temporaryFolder.getRoot())
.withBuildDirectory("target");

File jarPackageLock = new File(options.getNpmFolder(), "temp.json");
final String jarPackageLockContent = "{ \"jarData\"}";
FileUtils.write(jarPackageLock, jarPackageLockContent);

Mockito.when(finder
.getResource(DEV_BUNDLE_JAR_PATH + Constants.PACKAGE_LOCK_JSON))
.thenReturn(jarPackageLock.toURI().toURL());

BundleUtils.copyPackageLockFromBundle(options);

final String packageLockContents = FileUtils.readFileToString(
new File(options.getNpmFolder(), Constants.PACKAGE_LOCK_JSON),
StandardCharsets.UTF_8);

Assert.assertEquals("File should be gotten from jar on classpath",
jarPackageLockContent, packageLockContents);
}

@Test
public void pnpm_noPackageLockExists_devBundleLockYamlIsCopied_notJarLockOrJson()
throws IOException {
final Lookup lookup = Mockito.mock(Lookup.class);
ClassFinder finder = Mockito.mock(ClassFinder.class);
Mockito.when(lookup.lookup(ClassFinder.class)).thenReturn(finder);

Options options = new Options(lookup, temporaryFolder.getRoot())
.withBuildDirectory("target").withEnablePnpm(true);

File jarPackageLock = new File(options.getNpmFolder(), "temp.json");
final String jarPackageLockContent = "{ \"jarData\"}";
FileUtils.write(jarPackageLock, jarPackageLockContent);

Mockito.when(finder
.getResource(DEV_BUNDLE_JAR_PATH + Constants.PACKAGE_LOCK_YAML))
.thenReturn(jarPackageLock.toURI().toURL());

File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
devBundleFolder.mkdirs();
File devPackageLockJson = new File(devBundleFolder,
Constants.PACKAGE_LOCK_JSON);
File devPackageLock = new File(devBundleFolder,
Constants.PACKAGE_LOCK_YAML);

final String packageLockContent = "{ \"bundleFile\"}";
FileUtils.write(devPackageLock, packageLockContent);
FileUtils.write(devPackageLockJson, "{ \"json\"}");

BundleUtils.copyPackageLockFromBundle(options);

final String packageLockContents = FileUtils.readFileToString(
new File(options.getNpmFolder(), Constants.PACKAGE_LOCK_YAML),
StandardCharsets.UTF_8);

Assert.assertEquals("dev-bundle file should be used",
packageLockContent, packageLockContents);
}

@Test
public void pnpm_packageLockExists_nothingIsCopied() throws IOException {
Options options = new Options(Mockito.mock(Lookup.class),
temporaryFolder.getRoot()).withBuildDirectory("target")
.withEnablePnpm(true);

File packageLockFile = temporaryFolder
.newFile(Constants.PACKAGE_LOCK_YAML);
File devBundleFolder = new File(options.getNpmFolder(),
Constants.DEV_BUNDLE_LOCATION);
devBundleFolder.mkdirs();
File devPackageLockJson = new File(devBundleFolder,
Constants.PACKAGE_LOCK_YAML);

final String existingLockFile = "{ \"existing\" }";
FileUtils.write(packageLockFile, existingLockFile);

FileUtils.write(devPackageLockJson, "{ \"bundleFile\"}");

BundleUtils.copyPackageLockFromBundle(options);

final String packageLockContents = FileUtils
.readFileToString(packageLockFile, StandardCharsets.UTF_8);

Assert.assertEquals("Existing file should not be overwritten",
existingLockFile, packageLockContents);
}
}

0 comments on commit 1b668e5

Please sign in to comment.