Skip to content

Commit

Permalink
feat: add license check for production bundle (#16728)
Browse files Browse the repository at this point in the history
Collect pro-components from stats.json
which are used in the project and check
license for those.

Closes #16720
  • Loading branch information
caalador committed May 8, 2023
1 parent 46e8f43 commit 5d64929
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 62 deletions.
Expand Up @@ -19,6 +19,7 @@ import com.vaadin.flow.plugin.base.BuildFrontendUtil
import com.vaadin.flow.server.Constants
import com.vaadin.flow.server.frontend.BundleValidationUtil
import com.vaadin.flow.server.frontend.FrontendUtils
import com.vaadin.pro.licensechecker.LicenseChecker
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.Jar
Expand Down Expand Up @@ -74,9 +75,9 @@ public open class VaadinBuildFrontendTask : DefaultTask() {
if (adapter.generateBundle() && BundleValidationUtil.needsBundleBuild
(adapter.servletResourceOutputDirectory())) {
BuildFrontendUtil.runFrontendBuild(adapter)
} else {
logger.info("Not running webpack since generateBundle is false")
}
LicenseChecker.setStrictOffline(true)
BuildFrontendUtil.validateLicenses(adapter)

BuildFrontendUtil.updateBuildFile(adapter)
}
Expand Down
Expand Up @@ -37,6 +37,7 @@
import com.vaadin.flow.server.frontend.BundleValidationUtil;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.theme.Theme;
import com.vaadin.pro.licensechecker.LicenseChecker;

/**
* Goal that builds the frontend bundle.
Expand Down Expand Up @@ -127,6 +128,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {
exception);
}
}
LicenseChecker.setStrictOffline(true);
BuildFrontendUtil.validateLicenses(this);

BuildFrontendUtil.updateBuildFile(this);

Expand Down
Expand Up @@ -142,6 +142,12 @@ public void setup() throws Exception {
"jar-resources-source/META-INF/frontend");
jarResourcesSource.mkdirs();

File statsfile = new File(resourceOutputDirectory,
Constants.VAADIN_CONFIGURATION + "/stats.json");

statsfile.getParentFile().mkdirs();
FileUtils.fileWrite(statsfile, "UTF-8", "{}");

projectFrontendResourcesDirectory = new File(npmFolder,
"flow_resources");

Expand Down
Expand Up @@ -63,13 +63,15 @@
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.frontend.BundleValidationUtil;
import com.vaadin.flow.server.frontend.CvdlProducts;
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.FrontendToolsSettings;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.NodeTasks;
import com.vaadin.flow.server.frontend.Options;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.server.scanner.ReflectionsClassFinder;
import com.vaadin.flow.utils.FlowFileUtils;
import com.vaadin.pro.licensechecker.BuildType;
Expand Down Expand Up @@ -423,20 +425,17 @@ public static void runFrontendBuild(PluginAdapterBase adapter)
* - frontend tools access object
* @throws TimeoutException
* - while running vite
* @throws URISyntaxException
* - while parsing nodeDownloadRoot()) to URI
*/
public static void runVite(PluginAdapterBase adapter,
FrontendTools frontendTools)
throws TimeoutException, URISyntaxException {
FrontendTools frontendTools) throws TimeoutException {
runFrontendBuildTool(adapter, frontendTools, "Vite", "vite/bin/vite.js",
Collections.emptyMap(), "build");
}

private static void runFrontendBuildTool(PluginAdapterBase adapter,
FrontendTools frontendTools, String toolName, String executable,
Map<String, String> environment, String... params)
throws TimeoutException, URISyntaxException {
throws TimeoutException {

File buildExecutable = new File(adapter.npmFolder(),
NODE_MODULES + executable);
Expand Down Expand Up @@ -483,25 +482,44 @@ private static void runFrontendBuildTool(PluginAdapterBase adapter,
String.format("Failed to run %s due to an error", toolName),
e);
}

// Check License
validateLicenses(adapter);
}

private static void validateLicenses(PluginAdapterBase adapter) {
File nodeModulesFolder = new File(adapter.npmFolder(),
FrontendUtils.NODE_MODULES);

/**
* Validate pro component licenses.
*
* @param adapter
* the PluginAdapterBase
*/
public static void validateLicenses(PluginAdapterBase adapter) {
File outputFolder = adapter.webpackOutputDirectory();
File statsFile = new File(adapter.servletResourceOutputDirectory(),
Constants.VAADIN_CONFIGURATION + "/stats.json");

if (!statsFile.exists()) {
String statsJsonContent = null;
try {
// First check for compiled bundle
File statsFile = new File(adapter.servletResourceOutputDirectory(),
Constants.VAADIN_CONFIGURATION + "/stats.json");
if (!statsFile.exists()) {
// If no compiled bundle available check for jar-bundle
statsJsonContent = BundleValidationUtil
.findProdBundleStatsJson(adapter.getClassFinder());
} else {
statsJsonContent = IOUtils.toString(statsFile.toURI().toURL(),
StandardCharsets.UTF_8);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

if (statsJsonContent == null) {
// without stats.json in bundle we can not say if it is up-to-date
throw new RuntimeException(
"Stats file " + statsFile + " does not exist");
"No production bundle stats.json available.");
}

FrontendDependenciesScanner scanner = new FrontendDependenciesScanner.FrontendDependenciesScannerFactory()
.createScanner(true, adapter.getClassFinder(), true, null);
List<Product> commercialComponents = findCommercialFrontendComponents(
nodeModulesFolder, statsFile);
scanner, statsJsonContent);
commercialComponents.addAll(findCommercialJavaComponents(adapter));

for (Product component : commercialComponents) {
Expand Down Expand Up @@ -529,23 +547,24 @@ private static Logger getLogger() {
}

static List<Product> findCommercialFrontendComponents(
File nodeModulesFolder, File statsFile) {
FrontendDependenciesScanner scanner, String statsJsonContent) {
List<Product> components = new ArrayList<>();
try (InputStream in = new FileInputStream(statsFile)) {
String contents = IOUtils.toString(in, StandardCharsets.UTF_8);
JsonObject npmModules = Json.parse(contents)
.getObject("npmModules");
for (String npmModule : npmModules.keys()) {
Product product = CvdlProducts
.getProductIfCvdl(nodeModulesFolder, npmModule);
if (product != null) {
components.add(product);

final JsonObject statsJson = Json.parse(statsJsonContent);

if (statsJson.hasKey("cvdlModules")) {
final JsonObject cvdlModules = statsJson.getObject("cvdlModules");
for (String key : cvdlModules.keys()) {
if (!scanner.getPackages().containsKey(key)) {
// If product is not used do not collect it.
continue;
}
final JsonObject cvdlModule = cvdlModules.getObject(key);
components.add(new Product(cvdlModule.getString("name"),
cvdlModule.getString("version")));
}
return components;
} catch (Exception e) {
throw new RuntimeException("Error reading file " + statsFile, e);
}
return components;
}

static List<Product> findCommercialJavaComponents(
Expand Down Expand Up @@ -580,16 +599,6 @@ static List<Product> findCommercialJavaComponents(
return components;
}

private static FeatureFlags getFeatureFlags(PluginAdapterBase adapter) {
ClassFinder classFinder = adapter.getClassFinder();

Lookup lookup = adapter.createLookup(classFinder);

final FeatureFlags featureFlags = new FeatureFlags(lookup);
featureFlags.setPropertiesLocation(adapter.javaResourceFolder());
return featureFlags;
}

/**
* Updates the build info after the bundle has been built by build-frontend.
* <p>
Expand Down
Expand Up @@ -6,7 +6,9 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
Expand All @@ -29,6 +31,7 @@
import com.vaadin.flow.server.frontend.TaskRunNpmInstall;
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.utils.LookupImpl;
import com.vaadin.pro.licensechecker.Product;

Expand Down Expand Up @@ -140,21 +143,32 @@ public void should_useHillaEngine_withNodeUpdater()
}

@Test
public void detectsCommercialComponents()
throws URISyntaxException, ExecutionFailedException, IOException {

try (FileOutputStream out = new FileOutputStream(statsJson)) {
IOUtils.write(
"{\"npmModules\":{\"component\":\"1.2.3\", \"comm-component\":\"4.6.5\"}}",
out, StandardCharsets.UTF_8);
}
public void detectsCommercialComponents() {

// @formatter:off
String statsJson = "{"
+ " \"cvdlModules\": { "
+ " \"component\": {"
+ " \"name\": \"component\","
+ " \"version\":\"1.2.3\""
+ " }, "
+ " \"comm-component\": {"
+ " \"name\":\"comm-comp\","
+ " \"version\":\"4.6.5\""
+ " }"
+ " }"
+ "}";
// @formatter:on

final FrontendDependenciesScanner scanner = Mockito
.mock(FrontendDependenciesScanner.class);
Map<String, String> packages = new HashMap<>();
packages.put("comm-component", "4.6.5");
packages.put("@vaadin/button", "1.2.1");
Mockito.when(scanner.getPackages()).thenReturn(packages);

File nodeModulesFolder = new File(baseDir, "node_modules");
writePackageJson(nodeModulesFolder, "component", "1.2.3", null);
writePackageJson(nodeModulesFolder, "comm-component", "4.6.5",
"comm-comp");
List<Product> components = BuildFrontendUtil
.findCommercialFrontendComponents(nodeModulesFolder, statsJson);
.findCommercialFrontendComponents(scanner, statsJson);
Assert.assertEquals(1, components.size());
Assert.assertEquals("comm-comp", components.get(0).getName());
Assert.assertEquals("4.6.5", components.get(0).getVersion());
Expand Down
Expand Up @@ -31,6 +31,9 @@
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner;
import com.vaadin.flow.server.webcomponent.WebComponentExporterTagExtractor;
import com.vaadin.flow.server.webcomponent.WebComponentExporterUtils;
import com.vaadin.pro.licensechecker.BuildType;
import com.vaadin.pro.licensechecker.LicenseChecker;
import com.vaadin.pro.licensechecker.Product;

import elemental.json.Json;
import elemental.json.JsonArray;
Expand Down Expand Up @@ -116,27 +119,37 @@ private static boolean needsBuildDevBundle(Options options,
}

String statsJsonContent = DevBundleUtils.findBundleStatsJson(npmFolder);

if (statsJsonContent == null) {
// without stats.json in bundle we can not say if it is up-to-date
getLogger().info(
"No bundle's stats.json found for dev-bundle validation.");
return true;
}

return needsBuildInternal(options, frontendDependencies, finder,
statsJsonContent);
}

private static boolean needsBuildProdBundle(Options options,
FrontendDependenciesScanner frontendDependencies,
ClassFinder finder) throws IOException {
String statsJsonContent = BundleValidationUtil
.findProdBundleStatsJson(finder);
String statsJsonContent = findProdBundleStatsJson(finder);

if (statsJsonContent == null) {
// without stats.json in bundle we can not say if it is up-to-date
getLogger().info(
"No bundle's stats.json found for production-bundle validation.");
return true;
}

return needsBuildInternal(options, frontendDependencies, finder,
statsJsonContent);
}

private static boolean needsBuildInternal(Options options,
FrontendDependenciesScanner frontendDependencies,
ClassFinder finder, String statsJsonContent) throws IOException {
if (statsJsonContent == null) {
// without stats.json in bundle we can not say if it is up-to-date
getLogger().info("No bundle's stats.json found for validation.");
return true;
}

JsonObject packageJson = getPackageJson(options, frontendDependencies,
finder);
Expand Down
7 changes: 7 additions & 0 deletions flow-server/src/main/resources/vite.generated.ts
Expand Up @@ -226,6 +226,8 @@ function statsExtracterPlugin(): PluginOption {
.sort()
.filter((value, index, self) => self.indexOf(value) === index);
const npmModuleAndVersion = Object.fromEntries(npmModules.map((module) => [module, getVersion(module)]));
const cvdls = Object.fromEntries(npmModules.filter((module) => getCvdlName(module) != null)
.map((module) => [module, {name: getCvdlName(module),version: getVersion(module)}]));

mkdirSync(path.dirname(statsFile), { recursive: true });
const projectPackageJson = JSON.parse(readFileSync(projectPackageJsonFile, { encoding: 'utf-8' }));
Expand Down Expand Up @@ -337,6 +339,7 @@ function statsExtracterPlugin(): PluginOption {
themeJsonContents: themeJsonContents,
entryScripts,
webComponents,
cvdlModules: cvdls,
packageJsonHash: projectPackageJson?.vaadin?.hash
};
writeFileSync(statsFile, JSON.stringify(stats, null, 1));
Expand Down Expand Up @@ -782,3 +785,7 @@ function getVersion(module: string): string {
const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).version;
}
function getCvdlName(module: string): string {
const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).cvdlName;
}

0 comments on commit 5d64929

Please sign in to comment.