Introduction
Like many software projects, frameworks, plugins they start off because the author could not find a solution that really fitted their needs and submitting pull requests to existing projects would not have changed the direction of such projects to the needs of the author. This is one of those kind of projects and it is offered against a number of alternative solutions that are available as Gradle plugins.
The aim with the group of plugins is making the integration of Node.js and related technologies into the build automation pipeline as smooth as possible. This brought with it a number of subgoals:
-
Simplicity to use defaults - convention over configuration
-
Maximum flexibility if you need it.
-
Removing as many Node.js hurdles from the learning curve as possible.
-
No need to install
node
or relevant tools - let Gradle take care of it for you. -
Allow other developers/contributors of your projects to build it with out fiddling with a big list of prerequisites.
-
Isolate builds from potential side effects due to global
node
installations.
This is an incubating project. Until is 1.0 released one day, interfaces and DSL may change between 0.x releases.
|
Alternative solutions
This is not the only solution. You might also want to look at
-
Palantir NPM plugin - A Gradle Plugin to create lifecycle tasks that trigger npm run commands.
-
Craig Burke’s Client Dependencies plugin: Install client side dependencies from NPM or Bower by declaring dependencies in build.gradle].
-
Moowork’s Node plugin: Gradle plugin for executing node scripts (supports NPM, Yarn, Grunt and Gulp).
Bootstrapping
These plugins are available from the plugin portal. Add the appropriate plugin identifiers to your build.gradle
file depending on the type of functionality you require.
plugins {
id 'org.ysb33r.nodejs.base' version '0.6.2' // <1>
id 'org.ysb33r.nodejs.npm' version '0.6.2' // <2>
id 'org.ysb33r.nodejs.gulp' version '0.6.2' // <3>
}
1 | Base plugin |
2 | Support for using NPM |
3 | Support for using Gulp |
You need at least Gradle 3.3 to use these plugins. |
Base plugin
The base plugin provides:
NPM plugin
The NPM plugin provides:
Applying the NPM plugin will apply the base plugin.
Gulp plugin
The Gulp plugin provides:
-
gulp
project and task extensions -
GulpTask
task type.
Applying the Gulp plugin will apply the NPM plugin.
Working with Node
Configure global Node defaults
The Node.js distribution can be supplied in three possible ways:
nodejs {
executable version: '7.10.0' (1)
}
nodejs {
executable path: '/path/to/node' (2)
}
nodejs {
executable searchPath() (3)
}
1 | Supply a version and Gradle will take care of downloading the distribution and caching it. It will then used the cached version for running node . This is the preferred way of running Node.js with Gradle as it offers better control over reproducible builds. |
2 | Supply a path to the node executable. This works for people that have Node.js installed in standard locations. |
3 | Tell Gradle to look for node (or node.exe ) in the system search path. |
If no executable is configured, then Gradle will attempt to download the version specified in NodeJSExtension.NODEJS_DEFAULT. If Gradle runs on a non-supported platform this will fail.
Using nodeexec
It is possible to run node
directly from Gradle using the nodeexec
project execution extension. It operates in a similar fashion to Gradle’s Exec task, but it has it’s own node
nuances, which the user need to setup correctly.
Unresolved directive in parts/configure-node-defaults.adoc - include::/builds/ysb33rOrg/nodejs-gradle-plugin/src/downloadTest/groovy/org/ysb33r/gradle/nodejs/downloadtest/impl/NodeJSExecutorSpec.groovy[tags=nodeexec-with-closure,indent=0]
1 | Specify the name of the script. It can be absolute of relative to the working directory. |
2 | Specify any arguments that will be passed to the script. |
3 | Set the working directory for the execution. If this is something installed via NPM, then using the location of the NPM homedirectory from the npm extension will simplify life. |
4 | Specify the locaton of the node executable. There are various ways of doig this, but a easy way is to ask the node extension to resolve it. |
Consult the NodeJSExecSpec API documentation for full details of all of the settings.
Platform installation support
These plugins can automatically download, cache and render Node for the following platforms:
-
Linux 32 & 64-bit.
-
Mac 64-bit.
-
Windows 32 & 64-bit.
Should you need to run Gradle on a platform not listed above, but which Node supports and on which Gradle can run, you will need to configure the Node executable via the path
or search
methods. You can also raise an issue to ask for the support or you can submit a PR with the solution.
Working with NPM
Configure global NPM defaults
The location of npm-cli.js
can be supplied in four possible ways:
npm {
executable version: '4.5.0' (1)
}
npm {
executable path: '/path/to/npm' (2)
}
npm {
executable searchPath() (3)
}
npm {
executable defaultNodejs() (4)
}
1 | Supply a version and Gradle will take care of downloading the specific version of npm caching it. It will then run the cached version. This is an option for people who want to run a version of npm other than the one that ships with Node.js. |
2 | Supply a path to the npm executable. This works for people that have npm installed in predefined locations. |
3 | Tell Gradle to look for npm (or npm.cmd ) in the system search path, then infer loation of npm-cli.js . |
4 | Use the version on npm-cli.js that ships with the default version of Node.js. This is also the default operation if nothing is configured. |
Gradle does not use the npm or npm.cmd scripts. It uses npm-cli.js directly.
|
The location of the global and local npmrc
files can also be set.
npm {
localConfig "${projectDir}/npmrc2"
globalConfig "${project.rootProject.projectDir}/npmrc2"
}
The default local configuration is always set to an npmrc file located in the root of the root project and the global configuration is set to ~/.gradle/npmrc . This is specifically done so that configuration can be set on a project-wide basis for a Gradle project without interference from a possible globally installed Node.js. Placement of global configuration in the Gradle User Home allows a person to set specific global options for Node projects that are built by Gradle.
|
Dependency management
The NPM plugin adds a npmPackage
extension that can be used inside the dependencies
block to add NPM dependencies. For convenience it can be used along with the npm
configuration that is also added by the same plugin, however nothing prevents you from adding NPM dependencies to other configurations as well.
dependencies {
npm npmPackage(name: 'stringz', tag: '0.2.2')
}
Anything added with npmPackage
can also update package.json
depending on the type
property.
Current properties that can be used are:
-
scope
- NPM scope -
name
- NPM package name -
tag
- NPM tag -
type
- One ofprod
,dev
,optional
. If not supplied, will default toprod
. -
install-args
- Additional arguments that NPM has to use during installation. -
path
- Modify the sysstem path when installing this package. By default packages are installed with a very restricted environment. For instance, if the package installation requires access to/usr/bin
, then this can be set here.
In 0.1 the transitive logic handling is still pretty crude. |
NpmTask type
NpmTask
is a generic task type for running NPM commands. In its simplest form it takes an npm
command and a collection or arguments.
task createPackageJson( type : org.ysb33r.gradle.nodejs.tasks.NpmTask ) {
command 'init' (1)
cmdArgs '-f', '-y' (2)
}
1 | npm command |
2 | Arguments that are applicable to the specific command |
Customisation
By default an NpmTask
will obtain the location of node
from the nodejs project extension and any NPM configuration from the npm project extension. However, this plugin allows you to have a lot of flexibility should you need it. Therefore you can override any of the settings in both of the aforementioned extensions in task extensions by the same name. This allows you to for instance run a specific task with a different version of node
than the global configured version.
By taking the same previous example, you can do
task createPackageJson( type : org.ysb33r.gradle.nodejs.tasks.NpmTask ) {
command 'init'
cmdArgs '-f', '-y'
nodejs {
executable version : '7.10.0' (1)
}
npm {
localConfig "${projectDir}/npmrc2" (2)
}
}
1 | Change the specific of node when executing this task. |
2 | Use a different local configuration file. |
This kind of flexibility is not applicable for probably the majority of cases, but there are specific use cases where this is extremely helpful. |
Any other parameters that you would expect to see in an Gradle ExecSpec
can be used as well.
Working with Gulp
Configuring Gulp
Support for working with Gulp is available via the Gulp plugin (org.ysb33r.nodejs.gulp
). It adds a gulp
extension which is the place to configure global settings for working with Gulp.
gulp {
executable version: '1.2.3' (1)
gulpFile 'foo/gulpfile.js' (2)
requires 'foo', 'bar' (3)
}
1 | Tell Gradle where to find Gulp. The recommended approach is just to specify a version. if nothing is configured gradle will use a default version set with the plugin. See GulpExtension.GULP_DEFAULT. |
2 | Set the location of gulpfile.js . The default is "${project.projectDir}/gulpfile.js" . This causes Gulp to chage working directory to the location of this file. It does however still keep the original working directory from NPM in mind as well. |
3 | Add additional requires that will be passed as --requires to Gulp. |
In most cases the default settings should suffice and the user will not need to configure anything extra.
Gulp users will be familiar with using task as the entity of work to be processed by the build tool. In this plugin the keyword target is used to signify a Gulp task. This is done to prevent accidental clashes with the common Gradle keyword called task .
|
GulpTask type
myGulpTask {
target 'clean' (1)
}
1 | Set the target task to execute. If nothing is configured, the default Gulp task will be executed. |
In some cases the global Gulp configuration might not be suitable and per-task customisations can be done by accessing nodejs
, npm
and gulp
extensions on the task itself. the task will always look at the local extensions first before retrieving values from the global extensions.
myGulpTask {
gulp {
gulpFile 'foo/gulpfile.js' (1)
}
npm {
homeDirectory 'foo' (2)
}
}
1 | Override the location of gulpfile.js for this specific task. |
2 | Use a different NPM setup to run this task. |
This shows the flexibility of the plugin and how behaviour can be changed to deal with very specific situations. In most cases you will not need it, but when you need to do something extraordinary, it shuol dbe able to be configured.
Change dependency group
By default Gulp will be added to devDependencies
. It is possible to override and make it a default or optional dependency.
gulp {
installGroup NpmDependencyGroup.OPTIONAL (1)
}
1 | See NpmDependencyGroup for valid settings. |
Advanced Topics
Wrapping NPM packages
If you are familiar with NPM just will know that many packages have some entry point script which can be used as a tool on the command-line. You are probably quite familiar with Gulp too and if you already usedh the Gulp functionality that comes with the Gulp plugin, you might be pondering doing the same approach for your favourite Node tool package.
The process if doing this is relatively straight-forward. it involved creating an extension and a resolver. Once you have these you can continue implementing your own task types.
The following example are shown in Groovy, but you can use Java or Kotlin for your implementation if you wish. |
This approach can be used in a gradle script, buildSrc
or a standalone plugin.
In 0.1 it was still necessary to create a separate resolver. As from 0.2 this is no longer necessary and you can proceed to just creating the extension. |
Creating the extension
Assume for the moment that there is a packaged tool called FooBar which you want to wrap.
Firstly, create an extension class that extends AbstractPackageWrappingExtension.
There are three methods in protected scope that you will need to implement in addition to two contructors
@CompileStatic
class FooBarExtension extends AbstractPackageWrappingExtension {
FooBarExtension(Project project) { (1)
super(project)
}
FooBarExtension(Task task) { (2)
super(task,'foobar') (3)
}
@Override
protected String getExtensionName() { (4)
'name-of-this-extension'
}
@Override
protected String getEntryPoint() { (5)
'bin/foobar.js'
}
}
1 | Attaches the extension to a project. |
2 | Attaches the extension to a task |
3 | It is necessary top pass the name of the project extension, when creating the task extension. |
4 | The name that this extension will be known by. This is used by the superclass to resolve project extensions when the extensions are attached to a task. (This functionality might be removed in future). |
5 | This is the entrypoint script file and must be specified relative to the package folder after installation. |
You can now add methods to your extension as if necessary to reflect the functionality of the wrapped package. For instance the GulpExtension class adds gulpFile
and requires
to deal with the --gulpfile
and --requires
parameters of gulp.js
.
If you add methods where the returned values can be overriden in a task in a similar fashion to what can be done in NpmTask
and GulpTask
, you will need to add some logic to handle those cases. Here is an example of how it is being done in GulpExtension
to retrieve the value for gulpFile
.
File getGulpFile() {
if (task) { (1)
this.gulpFile != null ? getProject().file(this.gulpFile) : ((GulpExtension) projectExtension).getGulpFile()
} else {
getProject().file(this.gulpFile)
}
}
1 | If this is attached to a task, check if the value is set. Use it if it is, otherwise defer to the project extension. If this is a project extension use the local value. |
Create a task type
In some cases you might want to also add a task type, similar to GulpTask
. One approach is to extend AbstractNodeBaseTask.
@CompileStatic
class FooTask extends AbstractNodeBaseTask {
FooTask() {
super()
fooExtension = (FooExtension)(extensions.create('foo',FooExtension,this))
}
@TaskAction
void exec() {
NodeJSExecSpec execSpec = createExecSpec() (1)
execSpec.script fooExtension.resolvedExecutable.executable.absolutePath (2)
/* Configure execution specification against any properties */
runExecSpec(execSpec) (3)
}
private FooExtension fooExtension
}
1 | Base class has ability to create and pre-configure an execition specification. |
2 | Resolve the script from the resolver. |
3 | Execute the execution specification. |
Tips and tricks
Overriding Node Repository URI
There are circumstances where you might want to download Node distributions from elsewhere besides the standard repository. Simply set the system property org.ysb33r.gradle.nodejs.uri
to the alternative URI.