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

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.

build.gradle
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:

  • nodejs extension.

  • nodeexec execution extension.

  • Ability to download and use Node distributions.

NPM plugin

The NPM plugin provides:

  • npm extension.

  • NpmTask type.

  • npmexec project extension to run NPM commands.

  • npm, npmDevOnly, npmOptional configurations.

  • npmPackage for listing packages in Gradle dependencies blocks.

  • NpmExecSpec.

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:

build.gradle
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:

build.gradle
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.

build.gradle
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 of prod, dev, optional. If not supplied, will default to prod.

  • 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.

build.gradle
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

build.gradle
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.

build.gradle
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.

build.gradle
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.

GulpExtension.groovy
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.

FooTask.groovy
@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.