Skip to content

Commit

Permalink
feat: Enable running npm ci mode (CP: 23.4) (#17994)
Browse files Browse the repository at this point in the history
* feat: Enable running npm ci mode (#15579) (#15986)

Adds possibility to install the npm packages using npm ci instead of npm install (or for pnpm: pnpm install --frozen-lockfile) to install the exact versions in the lock file. Can be enabled with ci.build parameter when executing a production mode frontend build.

Fixes #15579

(cherry picked from commit 5607864)

* Fix npm ci test

---------

Co-authored-by: Teppo Kurki <teppo.kurki@vaadin.com>
  • Loading branch information
mshabarov and tepi authored Nov 8, 2023
1 parent c21c0a4 commit e62da3a
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,6 @@ internal class GradlePluginAdapter(val project: Project, private val isBeforePro
return extension.projectBuildDir
}
override fun postinstallPackages(): List<String> = extension.postinstallPackages

override fun ciBuild(): Boolean = extension.ciBuild
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ public open class VaadinFlowPluginExtension(project: Project) {
*/
public var processResourcesTaskName : String? = null

/**
* Setting this to true will run {@code npm ci} instead of {@code npm install} when using npm.
*
* If using pnpm, the install will be run with {@code --frozen-lockfile} parameter.
*
* This makes sure that the versions in package lock file will not be overwritten and production builds are reproducible.
*/
public var ciBuild: Boolean = false

public fun filterClasspath(@DelegatesTo(value = ClasspathFilter::class, strategy = Closure.DELEGATE_FIRST) block: Closure<*>? = null): ClasspathFilter {
if (block != null) {
block.delegate = classpathFilter
Expand Down Expand Up @@ -262,6 +271,11 @@ public open class VaadinFlowPluginExtension(project: Project) {
pnpmEnable = pnpmEnableProperty
}

val ciBuildProperty: Boolean? = project.getBooleanProperty(InitParameters.CI_BUILD)
if (ciBuildProperty != null) {
ciBuild = ciBuildProperty
}

val useGlobalPnpmProperty: Boolean? = project.getBooleanProperty(InitParameters.SERVLET_PARAMETER_GLOBAL_PNPM)
if (useGlobalPnpmProperty != null) {
useGlobalPnpm = useGlobalPnpmProperty
Expand Down Expand Up @@ -298,6 +312,7 @@ public open class VaadinFlowPluginExtension(project: Project) {
"frontendResourcesDirectory=$frontendResourcesDirectory, " +
"optimizeBundle=$optimizeBundle, " +
"pnpmEnable=$pnpmEnable, " +
"ciBuild=$ciBuild, " +
"useGlobalPnpm=$useGlobalPnpm, " +
"requireHomeNodeExec=$requireHomeNodeExec, " +
"useDeprecatedV14Bootstrapping=$useDeprecatedV14Bootstrapping, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.vaadin.flow.plugin.base.PluginAdapterBuild;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.theme.Theme;

Expand Down Expand Up @@ -92,6 +93,19 @@ public class BuildFrontendMojo extends FlowModeAbstractMojo
@Parameter(defaultValue = "true")
private boolean optimizeBundle;

/**
* Setting this to true will run {@code npm ci} instead of
* {@code npm install} when using npm.
*
* If using pnpm, the install will be run with {@code --frozen-lockfile}
* parameter.
*
* This makes sure that the versions in package lock file will not be
* overwritten and production builds are reproducible.
*/
@Parameter(property = InitParameters.CI_BUILD, defaultValue = "false")
private boolean ciBuild;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
long start = System.nanoTime();
Expand Down Expand Up @@ -148,4 +162,9 @@ public boolean runNpmInstall() {
return runNpmInstall;
}

@Override
public boolean ciBuild() {
return ciBuild;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ public void existingTokenFile_enableDevServerShouldBeAdded()
initialBuildInfo.put(InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, true);
initialBuildInfo.put(
InitParameters.SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE, true);
initialBuildInfo.put(InitParameters.CI_BUILD, true);

org.apache.commons.io.FileUtils.forceMkdir(tokenFile.getParentFile());
org.apache.commons.io.FileUtils.write(tokenFile,
Expand All @@ -516,6 +517,8 @@ public void existingTokenFile_enableDevServerShouldBeAdded()
InitParameters.SERVLET_PARAMETER_ENABLE_PNPM
+ "should have been removed",
buildInfo.get(InitParameters.SERVLET_PARAMETER_ENABLE_PNPM));
Assert.assertNull(InitParameters.CI_BUILD + "should have been removed",
buildInfo.get(InitParameters.CI_BUILD));
Assert.assertNull(
InitParameters.REQUIRE_HOME_NODE_EXECUTABLE
+ "should have been removed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ public static void runNodeUpdater(PluginAdapterBuild adapter)
.withNodeDownloadRoot(nodeDownloadRootURI)
.setNodeAutoUpdate(adapter.nodeAutoUpdate())
.setJavaResourceFolder(adapter.javaResourceFolder())
.withPostinstallPackages(adapter.postinstallPackages());
.withPostinstallPackages(adapter.postinstallPackages())
.withCiBuild(adapter.ciBuild());
new NodeTasks(options).execute();
} catch (ExecutionFailedException exception) {
throw exception;
Expand Down Expand Up @@ -603,6 +604,7 @@ public static void updateBuildFile(PluginAdapterBuild adapter) {
buildInfo.remove(GENERATED_TOKEN);
buildInfo.remove(FRONTEND_TOKEN);
buildInfo.remove(InitParameters.SERVLET_PARAMETER_ENABLE_PNPM);
buildInfo.remove(InitParameters.CI_BUILD);
buildInfo.remove(InitParameters.REQUIRE_HOME_NODE_EXECUTABLE);
buildInfo.remove(
InitParameters.SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,16 @@ public interface PluginAdapterBuild extends PluginAdapterBase {
*/
boolean runNpmInstall();

/**
* Setting this to true will run {@code npm ci} instead of
* {@code npm install} when using npm.
*
* If using pnpm, the install will be run with {@code --frozen-lockfile}
* parameter.
*
* This makes sure that the package lock file will not be overwritten.
*
* @return true if ci build should be enabled
*/
boolean ciBuild();
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,9 @@ public class InitParameters implements Serializable {
* @since
*/
public static final String ADDITIONAL_POSTINSTALL_PACKAGES = "npm.postinstallPackages";

/**
* Configuration name for enabling ci build for npm/pnpm.
*/
public static final String CI_BUILD = "ci.build";
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public NodeTasks(Options options) {
options.enablePnpm, options.requireHomeNodeExec,
options.nodeVersion, options.nodeDownloadRoot,
options.useGlobalPnpm, options.nodeAutoUpdate,
options.postinstallPackages));
options.postinstallPackages, options.isCiBuild()));

commands.add(new TaskInstallWebpackPlugins(
new File(options.npmFolder, options.buildDirectory)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public class Options implements Serializable {

boolean enablePnpm = Constants.ENABLE_PNPM_DEFAULT;

private boolean ciBuild;

boolean useGlobalPnpm = false;

File endpointSourceFolder;
Expand Down Expand Up @@ -476,6 +478,21 @@ public Options enablePnpm(boolean enable) {
return this;
}

/**
* Enables ci build.
* <p>
* "npm ci" will be used instead of "npm install". "--frozen-lockfile" will
* be used if pnpm is used instead of npm.
*
* @param ciBuild
* true to enable ci build
* @return the builder, for chaining
*/
public Options withCiBuild(boolean ciBuild) {
this.ciBuild = ciBuild;
return this;
}

/**
* Uses globally installed pnpm tool for frontend packages installation.
*
Expand Down Expand Up @@ -647,4 +664,8 @@ public File getJarFrontendResourcesFolder() {
public ClassFinder getClassFinder() {
return classFinder;
}

public boolean isCiBuild() {
return ciBuild;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ public String getPackageManager() {
"pnpm", ".ignored_pnpm", ".pnpm", ".staging", ".vaadin",
MODULES_YAML);
private final boolean enablePnpm;

private final boolean ciBuild;
private final boolean requireHomeNodeExec;
private final boolean autoUpdate;

Expand Down Expand Up @@ -157,11 +159,13 @@ public String getPackageManager() {
* {@code true} to automatically update to a new node version
* @param additionalPostinstallPackages
* a list of packages to run postinstall for
* @param ciBuild
* whether to use "npm ci" instead of "npm install"
*/
TaskRunNpmInstall(NodeUpdater packageUpdater, boolean enablePnpm,
boolean requireHomeNodeExec, String nodeVersion,
URI nodeDownloadRoot, boolean useGlobalPnpm, boolean autoUpdate,
List<String> additionalPostinstallPackages) {
List<String> additionalPostinstallPackages, boolean ciBuild) {
this.packageUpdater = packageUpdater;
this.enablePnpm = enablePnpm;
this.requireHomeNodeExec = requireHomeNodeExec;
Expand All @@ -171,23 +175,34 @@ public String getPackageManager() {
this.autoUpdate = autoUpdate;
this.additionalPostinstallPackages = Objects
.requireNonNull(additionalPostinstallPackages);
this.ciBuild = ciBuild;
}

@Override
public void execute() throws ExecutionFailedException {
String toolName = enablePnpm ? "pnpm" : "npm";
String command = "install";
if (ciBuild) {
if (enablePnpm) {
command += " --frozen-lockfile";
} else {
command = "ci";
}
}

if (packageUpdater.modified || shouldRunNpmInstall()) {
packageUpdater.log().info("Running `" + toolName + " install` to "
packageUpdater.log().info("Running `" + toolName + " " + command
+ "` to "
+ "resolve and optionally download frontend dependencies. "
+ "This may take a moment, please stand by...");
runNpmInstall();

updateLocalHash();
} else {
packageUpdater.log().info(
"Skipping `{} install` because the frontend packages are already "
"Skipping `{} {}` because the frontend packages are already "
+ "installed in the folder '{}' and the hash in the file '{}' is the same as in '{}'",
toolName,
toolName, command,
packageUpdater.nodeModulesFolder.getAbsolutePath(),
packageUpdater.getVaadinJsonFile().getAbsolutePath(),
Constants.PACKAGE_JSON);
Expand Down Expand Up @@ -314,7 +329,7 @@ private void runNpmInstall() throws ExecutionFailedException {
createNpmRcFile();
} catch (IOException exception) {
logger.warn(".npmrc generation failed; pnpm "
+ "package installation may require manaually passing "
+ "package installation may require manually passing "
+ "the --shamefully-hoist flag", exception);
}
}
Expand Down Expand Up @@ -344,7 +359,17 @@ private void runNpmInstall() throws ExecutionFailedException {
}

npmInstallCommand.add("--ignore-scripts");
npmInstallCommand.add("install");

if (ciBuild) {
if (enablePnpm) {
npmInstallCommand.add("install");
npmInstallCommand.add("--frozen-lockfile");
} else {
npmInstallCommand.add("ci");
}
} else {
npmInstallCommand.add("install");
}

postinstallCommand.add("run");
postinstallCommand.add("postinstall");
Expand Down Expand Up @@ -618,21 +643,31 @@ private void cleanUp() throws ExecutionFailedException {
return;
}
long startTime = System.currentTimeMillis();
File modulesYaml = new File(packageUpdater.nodeModulesFolder,
MODULES_YAML);
boolean hasModulesYaml = modulesYaml.exists() && modulesYaml.isFile();
if (!enablePnpm && hasModulesYaml) {
deleteNodeModules(packageUpdater.nodeModulesFolder);
} else if (enablePnpm && !hasModulesYaml) {
// presence of .staging dir with a "pnpm-*" folder means that pnpm
// download is in progress, don't remove anything in this case
File staging = new File(packageUpdater.nodeModulesFolder,
".staging");
if (!staging.isDirectory() || staging.listFiles(
(dir, name) -> name.startsWith("pnpm-")).length == 0) {

if (ciBuild) {
File nodeModules = new File(packageUpdater.npmFolder,
FrontendUtils.NODE_MODULES);
deleteNodeModules(nodeModules);
} else {
File modulesYaml = new File(packageUpdater.nodeModulesFolder,
MODULES_YAML);
boolean hasModulesYaml = modulesYaml.exists()
&& modulesYaml.isFile();
if (!enablePnpm && hasModulesYaml) {
deleteNodeModules(packageUpdater.nodeModulesFolder);
} else if (enablePnpm && !hasModulesYaml) {
// presence of .staging dir with a "pnpm-*" folder means that
// pnpm
// download is in progress, don't remove anything in this case
File staging = new File(packageUpdater.nodeModulesFolder,
".staging");
if (!staging.isDirectory() || staging.listFiles(
(dir, name) -> name.startsWith("pnpm-")).length == 0) {
deleteNodeModules(packageUpdater.nodeModulesFolder);
}
}
}

lastInstallStats.cleanupTimeMs = System.currentTimeMillis() - startTime;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Logger log() {
new TaskRunNpmInstall(nodeUpdater, false, false,
FrontendTools.DEFAULT_NODE_VERSION,
URI.create(NodeInstaller.DEFAULT_NODEJS_DOWNLOAD_ROOT), false,
false, Collections.emptyList()).execute();
false, Collections.emptyList(), false).execute();

FrontendUtils.deleteNodeModules(new File(npmFolder, "node_modules"));

Expand Down
Loading

0 comments on commit e62da3a

Please sign in to comment.