rulesnode [](https://travis-ci.org/pubref/rulesnode)

April 14, 2019 ยท View on GitHub

Bazel NodeJs Yarn

rules_node Build Status

Rules

RuleDescription
node_repositoriesInstall node toolchain.
yarn_modulesInstall a set node module dependencies using yarn.
node_moduleDefine a node module from a set of source files (having an optional main (or index) entry point).
node_binaryRun a node module.
node_testRun a node binary as a bazel test.
mocha_testRun a mocha test script.

node_repositories

WORKSPACE rule that downloads and configures node based on your operating system. Includes node (8.15.1) and yarn (1.0.1).

RULES_NODE_COMMIT = '...' # Update to current HEAD
RULES_NODE_SHA256 = '...'

http_archive(
    name = "org_pubref_rules_node",
    url = "https://github.com/pubref/rules_node/archive/%s.zip" % RULES_NODE_COMMIT,
    strip_prefix = "rules_node-%s" % RULES_NODE_COMMIT,
    sha256 = RULES_NODE_SHA256,
)

load("@org_pubref_rules_node//node:rules.bzl", "node_repositories")

node_repositories()

yarn_modules

Install a set of module dependencies into a yarn_modules folder as an external workspace. Requires either a package.json file or deps as input.

# In WORKSPACE
load("@org_pubref_rules_node//node:rules.bzl", "yarn_modules")

# Use a package.json file as input. Location of the package json
# is arbitrary.
yarn_modules(
    name = "yarn_modules",
    package_json = "//:package.json",
)

# Shortcut form without a separate package.json file
yarn_modules(
    name = "yarn_modules",
    deps = {
        "react": "15.3.2",
        "react-dom": "15.3.2",
    },
)

How It Works:

  1. Create an external workspace @yarn_modules at $(bazel info output_base)/external/yarn_modules.

  2. Invoke yarn install to create a node_modules folder and populate it with the necessary dependencies.

  3. Read the generated yarn.lock file, parse it, and write out a @yarn_modules//:BUILD file. This file contains a node_module rule foreach entry in the yarn.lock file, a node_module rule with the special name _all_, and a node_binary rule foreach executable script in the node_modules/.bin folder.

Note 1: You can inspect all the targets by running bazel query @yarn_modules//:*.

Note 2: The workspace name yarn_modules is arbitrary, choose whatever you like (other than node_modules itself, that one doesn't work).

At this point you can use these rule targets as deps for your node_module rules. Example:

node_module(
    name = "my_module",
    package_json = "package.json",
    srcs = glob(["**/*.js"]),
    deps = [
        "@yarn_modules//:_all_",
    ],
)

yarn_module attributes

TypeNameDescription
optionallabelpackage_jsonA package.json file containing the dependencies that should be installed.
optionalstring_dictdepsA mapping of name --> version for the dependencies that should be installed.
optionalstring_listpost_installA list of command-line arguments that should be invoked after the yarn install step. See #55.

Either package_json or deps must be present, but not both.

node_module

BUILD file rule that creates a folder which conforms to the nodejs Folders as Modules packaging structure. Example:

node_module(
    name = "my_module",
    main = "index.js",
    srcs = [
        "lib/util.js",
        "lib/math.js",
    ],
    version = "1.2.0",
    description = "Example node module",
    deps = [
        "@yarn_modules//:lodash",
        "@yarn_modules//:fs-extra",
    ],

When used in a node_binary rule, this ultimately materializes to:

node_modules/my_module
node_modules/my_module/package.json
node_modules/my_module/index.js
node_modules/my_module/lib/util.js
node_modules/my_module/lib/math.js
node_modules/lodash
node_modules/fs-extra

When used by other node_module rules, you can import the module as:

const myModule = require("my_module");

There are three basic ways to create a node_module rule:

1. Creating a node_module with a package.json file

node_module(
    name = "my_module_1",
    package_json = "package.json", # label to the 'package.json' file to use directly
)

In this scenario, assumes the package.json file has an entry that specifies the main entrypoint (or not, if you follow the Files as Modules pattern).

2. Creating a node_module with a label to the main entrypoint source file

node_module(
    name = "my_module_2",
    main = "app.js", # label to the entrypoint file for the module
    version = "1.0.0", # optional arguments to populate the generated package.json file
    ...
)

In this scenario, a package.json file will be generated for the module that specifies the file you provide to the main attribute.

3. Creating a node_module with a label to the index.js entrypoint source file

node_module(
    name = "my_module_3",
    index = "index.js", # label to the 'index.js' file to use as the index
)

In this scenario, no package.json file is generated.

Module dependencies

Build up a dependency tree via the deps attribute:

node_module(
    name = "my_module_3",
    ...
    deps = [
        "@yarn_modules//:_all_", # special token '_all_' to have access to all modules
        ":my_module_1",
    ],
)

Core node_module attributes

TypeNameDefaultDescription
optionallabelpackage_jsonNoneExplicitly name a package.json file to use for the module.
optionallabelmainNoneSource file named in the generated package.json main property.
optionallabelindexNoneSource file to be used as the index file (supresses generation of a package.json file).
optionallabel_listsrcs[]Source files to be included in the module.
optionallabel_listdeps[]node_module rule dependencies.

node_module attributes that affect the name of the module

For reference, by default a node_module rule //src/js:my_module generates node_modules/src/js/my_module.

TypeNameDefaultDescription
optionalstringnamespaceNoneSee 1
optionalstringmodule_name${ctx.label.package}/{ctx.label.name}See 2
optionalstringseparator/See 3

1 Use to scope the module with some organization prefix. Example: namespace = '@foo' generates node_modules/@foo/src/js/my_module.

2 Override the module name. Example: name = 'barbaz' with namespace (above) generates node_modules/@foo/barbaz

3 Example: separator = '-' generates node_modules/src-js-my_module.

node_module attributes that affect the generated package.json

These are only relevant if you don't explicitly name a package.json file.

TypeNameDefaultDescription
optionalstringversion1.0.0Version string
optionalstringurlNoneUrl where the module tgz archive was resolved
optionalstringsha1NoneSha1 hash of of the resolved tgz archive
optionalstringdescriptionNoneModule description
optionalstring_dictexecutablesNoneA mapping from binary name to internal node module path. Example executables = { 'foo': 'bin/foo' }.

node_module attributes that affect the relative path of files included in the module

TypeNameDefaultDescription
optionalstringlayoutrelativeChanges the way files are included in the module. One of relative or workspace.

Consider a file with the label //src/js/my_module/app.js. With layout = 'relative' (the default), the location of the file becomes node_modules/src/js/my_module/app.js (skylark: file.short_path relative to module_name). Under layout = 'workspace', the it becomes node_modules/src/js/my_module/src/js/my_module/app.js (skylark: file.path). This is relevant only for protocol buffers where the generated sources import their own dependencies relative to the workspace, which needs to be preserved in the generated module.

node_binary

The node_binary rule writes a script to execute a module entrypoint.

load("@org_pubref_rules_node//node:rules.bzl", "node_binary")

node_binary(
    name = "foo",
    entrypoint = ":my_module_1",
)

In example above, we're specifying the name of a node_module to use as the entrypoint.

node_binary(
    name = "foo",
    main = "foo.js",
    deps = [
        ":my_module_1
    ],
)

In this second example, we're specifying the name of a file to use as the entrypoint (under the hood, it will just build a node_module (called foo_module) for your single main foo.js file entrypoint, becoming equivalent to the first example).

node_binary(
    name = "foo",
    entrypoint = ":my_module_2",
    executable = "baz",
)

In this third example (above), we're specifying the name of the node module to start with (my_module_2) and the name of the executable within my_module_2 to run (baz). In this case the node_module rule definition for my_module_2 must have a string_dict with an entry for baz (like executables = { 'baz': 'bin/baz' }.

Output structure of files generated for a node_binary rule

A node_binary rule named foo will create a folder having exactly two entries:

  1. An executable shell script named foo.
  2. A folder which bundles up all the needed files in foo_files/.

Within foo_files/, there will also be exactly two entries:

  1. The node executable itself.
  2. The node_modules/ folder with all the built/copied modules (including the entrypoint module).

Building a deployable bundle

To generate a tarred/gzipped archive of the above example that you can ship as a single 'executable' self-contained package, invoke $ bazel build :{target}_deploy.tar.gz. This is similar in intent to the java {target}_deploy.jar implicit build rule.

$ bazel build :foo_deploy
Target //:foo_deploy.tar.gz up-to-date:
  bazel-bin/foo_bundle.tgz
$ du -h bazel-bin/foo_bundle.tgz
33M bazel-bin/foo_bundle.tgz

node_test

The node_test rule is identical to node_binary, but sets the test = True flag such that it can be used as a bazel test.

mocha_test

Runs a mocha test identified by the start script given in main or module given in entrypoint.

Note: The mocha_test rule depends on @mocha_modules//:_all_, so you'll need to add this dependency in your WORKSPACE file:

yarn_modules(
    name = "mocha_modules",
    deps = {
        "mocha": "3.5.3",
    }
)
mocha_test(
    name = "test",
    main = "test.js",
)

Conclusion

That's it! Please refer to the various workspaces in tests/ and the source for more detail.