blob: ac2d9057ab4ea187ee4080ba32a98dc6f41e0411 [file]
"""npm packaging
Note, this is intended for sharing library code with non-Bazel consumers.
If all users of your library code use Bazel, they should just add your library
to the `deps` of one of their targets.
"""
load("//:providers.bzl", "DeclarationInfo", "JSEcmaScriptModuleInfo", "JSModuleInfo", "JSNamedModuleInfo", "LinkablePackageInfo", "NODE_CONTEXT_ATTRS", "NodeContextInfo")
_DOC = """The pkg_npm rule creates a directory containing a publishable npm artifact.
Example:
```python
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
pkg_npm(
name = "my_package",
srcs = ["package.json"],
deps = [":my_typescript_lib"],
substitutions = {"//internal/": "//"},
)
```
You can use a pair of `// BEGIN-INTERNAL ... // END-INTERNAL` comments to mark regions of files that should be elided during publishing.
For example:
```javascript
function doThing() {
// BEGIN-INTERNAL
// This is a secret internal-only comment
doInternalOnlyThing();
// END-INTERNAL
}
```
With the Bazel stamping feature, pkg_npm will replace any placeholder version in your package with the actual version control tag.
See the [stamping documentation](https://github.com/bazelbuild/rules_nodejs/blob/master/docs/index.md#stamping)
Usage:
`pkg_npm` yields four labels. Build the package directory using the default label:
```sh
$ bazel build :my_package
Target //:my_package up-to-date:
bazel-out/fastbuild/bin/my_package
$ ls -R bazel-out/fastbuild/bin/my_package
```
Dry-run of publishing to npm, calling `npm pack` (it builds the package first if needed):
```sh
$ bazel run :my_package.pack
INFO: Running command line: bazel-out/fastbuild/bin/my_package.pack
my-package-name-1.2.3.tgz
$ tar -tzf my-package-name-1.2.3.tgz
```
Actually publish the package with `npm publish` (also builds first):
```sh
# Check login credentials
$ bazel run @nodejs//:npm_node_repositories who
# Publishes the package
$ bazel run :my_package.publish
```
You can pass arguments to npm by escaping them from Bazel using a double-hyphen, for example:
`bazel run my_package.publish -- --tag=next`
It is also possible to use the resulting tar file file from the `.pack` as an action input via the `.tar` label.
To make use of this label, the `tgz` attribute must be set, and the generating `pkg_npm` rule must have a valid `package.json` file
as part of its sources:
```python
pkg_npm(
name = "my_package",
srcs = ["package.json"],
deps = [":my_typescript_lib"],
tgz = "my_package.tgz",
)
my_rule(
name = "foo",
srcs = [
"//:my_package.tar",
],
)
```
"""
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
PKG_NPM_ATTRS = dict(NODE_CONTEXT_ATTRS, **{
"deps": attr.label_list(
doc = """Other targets which produce files that should be included in the package, such as `rollup_bundle`""",
allow_files = True,
),
"nested_packages": attr.label_list(
doc = """Other pkg_npm rules whose content is copied into this package.""",
allow_files = True,
),
"package_name": attr.string(
doc = """The package name that the linker will link this npm package as.
If package_path is set, the linker will link this package under <package_path>/node_modules/<package_name>.
If package_path is not set the this will be the root node_modules of the workspace.""",
),
"package_path": attr.string(
doc = """The package path in the workspace that the linker will link this npm package to.
If package_path is set, the linker will link this package under <package_path>/node_modules/<package_name>.
If package_path is not set the this will be the root node_modules of the workspace.""",
),
"srcs": attr.label_list(
doc = """Files inside this directory which are simply copied into the package.""",
allow_files = True,
),
"substitutions": attr.string_dict(
doc = """Key-value pairs which are replaced in all the files while building the package.
You can use values from the workspace status command using curly braces, for example
`{"0.0.0-PLACEHOLDER": "{STABLE_GIT_VERSION}"}`.
See the section on stamping in the [README](stamping)
""",
),
"tgz": attr.string(
doc = """If set, will create a `.tgz` file that can be used as an input to another rule, the tar will be given the name assigned to this attribute.
NOTE: If this attribute is set, a valid `package.json` file must be included in the sources of this target
""",
),
"validate": attr.bool(
doc = "Whether to check that the attributes match the package.json",
default = True,
),
"vendor_external": attr.string_list(
doc = """External workspaces whose contents should be vendored into this workspace.
Avoids `external/foo` path segments in the resulting package.""",
),
"_npm_script_generator": attr.label(
default = Label("//internal/pkg_npm:npm_script_generator"),
cfg = "host",
executable = True,
),
"_packager": attr.label(
default = Label("//internal/pkg_npm:packager"),
cfg = "host",
executable = True,
),
"_run_npm_bat_template": attr.label(
default = Label("@nodejs//:run_npm.bat.template"),
allow_single_file = True,
),
"_run_npm_template": attr.label(
default = Label("@nodejs//:run_npm.sh.template"),
allow_single_file = True,
),
})
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
PKG_NPM_OUTPUTS = {
"pack_bat": "%{name}.pack.bat",
"pack_sh": "%{name}.pack.sh",
"publish_bat": "%{name}.publish.bat",
"publish_sh": "%{name}.publish.sh",
}
# Takes a depset of files and returns a corresponding list of files without any files
# that aren't part of the specified package path. Also include files from external repositories
# that explicitly specified in the vendor_external list.
def _filter_out_external_files(ctx, files, package_path):
result = []
for file in files:
# NB: package_path may be an empty string
if file.short_path.startswith(package_path) and not file.short_path.startswith("../"):
result.append(file)
else:
for v in ctx.attr.vendor_external:
if file.short_path.startswith("../%s/" % v):
result.append(file)
return result
# Serializes a file into a struct that matches the `BazelFileInfo` type in the
# packager implementation. Useful for transmission of such information.
def _serialize_file(file):
return struct(path = file.path, shortPath = file.short_path)
# Serializes a list of files into a JSON string that can be passed as CLI argument
# for the packager, matching the `BazelFileInfo[]` type in the packager implementation.
def _serialize_files_for_arg(files):
result = []
for file in files:
result.append(_serialize_file(file))
return json.encode(result)
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
def create_package(ctx, deps_files, nested_packages):
"""Creates an action that produces the npm package.
It copies srcs and deps into the artifact and produces the .pack and .publish
scripts.
Args:
ctx: the starlark rule context
deps_files: list of files to include in the package which have been
specified as dependencies
nested_packages: list of TreeArtifact outputs from other actions which are
to be nested inside this package
Returns:
The tree artifact which is the publishable directory.
"""
stamp = ctx.attr.node_context_data[NodeContextInfo].stamp
all_files = deps_files + ctx.files.srcs
if not stamp and len(all_files) == 1 and all_files[0].is_directory and len(ctx.files.nested_packages) == 0:
# Special case where these is a single dep that is a directory artifact and there are no
# source files or nested_packages; in that case we assume the package is contained within
# that single directory and there is no work to do
package_dir = all_files[0]
_create_npm_scripts(ctx, package_dir)
return package_dir
package_dir = ctx.actions.declare_directory(ctx.label.name)
owning_package_name = ctx.label.package
# List of dependency sources which are local to the package that defines the current
# target. Also include files from external repositories that explicitly specified in
# the vendor_external list. We only want to package deps files which are inside of the
# current package unless explicitly specified.
filtered_deps_sources = _filter_out_external_files(ctx, deps_files, owning_package_name)
args = ctx.actions.args()
inputs = ctx.files.srcs + deps_files + nested_packages
args.use_param_file("%s", use_always = True)
args.add(package_dir.path)
args.add(owning_package_name)
args.add(_serialize_files_for_arg(ctx.files.srcs))
args.add(_serialize_files_for_arg(filtered_deps_sources))
args.add(_serialize_files_for_arg(nested_packages))
args.add(ctx.attr.substitutions)
if stamp:
# The version_file is an undocumented attribute of the ctx that lets us read the volatile-status.txt file
# produced by the --workspace_status_command.
# Similarly info_file reads the stable-status.txt file.
# That command will be executed whenever
# this action runs, so we get the latest version info on each execution.
# See https://github.com/bazelbuild/bazel/issues/1054
args.add(ctx.version_file.path)
inputs.append(ctx.version_file)
args.add(ctx.info_file.path)
inputs.append(ctx.info_file)
else:
args.add_all(["", ""])
args.add_joined(ctx.attr.vendor_external, join_with = ",", omit_if_empty = False)
args.add(str(ctx.label))
args.add(ctx.attr.validate)
args.add(ctx.attr.package_name)
ctx.actions.run(
progress_message = "Assembling npm package %s" % package_dir.short_path,
mnemonic = "AssembleNpmPackage",
executable = ctx.executable._packager,
inputs = inputs,
outputs = [package_dir],
arguments = [args],
)
_create_npm_scripts(ctx, package_dir)
return package_dir
def _create_npm_scripts(ctx, package_dir):
args = ctx.actions.args()
args.add_all([
package_dir.path,
ctx.outputs.pack_sh.path,
ctx.outputs.publish_sh.path,
ctx.file._run_npm_template.path,
ctx.outputs.pack_bat.path,
ctx.outputs.publish_bat.path,
ctx.file._run_npm_bat_template.path,
])
ctx.actions.run(
progress_message = "Generating npm pack & publish scripts",
mnemonic = "GenerateNpmScripts",
executable = ctx.executable._npm_script_generator,
inputs = [ctx.file._run_npm_template, ctx.file._run_npm_bat_template, package_dir],
outputs = [ctx.outputs.pack_sh, ctx.outputs.publish_sh, ctx.outputs.pack_bat, ctx.outputs.publish_bat],
arguments = [args],
# Must be run local (no sandbox) so that the pwd is the actual execroot
# in the script which is used to generate the path in the pack & publish
# scripts.
execution_requirements = {"local": "1"},
)
def _pkg_npm(ctx):
deps_files_depsets = []
for dep in ctx.attr.deps:
# Collect whatever is in the "data"
deps_files_depsets.append(dep.data_runfiles.files)
# Only collect DefaultInfo files (not transitive)
deps_files_depsets.append(dep.files)
# All direct & transitive JavaScript-producing deps
if JSModuleInfo in dep:
deps_files_depsets.append(dep[JSModuleInfo].sources)
# All direct and transitive deps that produce CommonJS modules
if JSNamedModuleInfo in dep:
deps_files_depsets.append(dep[JSNamedModuleInfo].sources)
# All direct and transitive deps that produce ES6 modules
if JSEcmaScriptModuleInfo in dep:
deps_files_depsets.append(dep[JSEcmaScriptModuleInfo].sources)
# Include all transitive declarations
if DeclarationInfo in dep:
deps_files_depsets.append(dep[DeclarationInfo].transitive_declarations)
# Note: to_list() should be called once per rule!
deps_files = depset(transitive = deps_files_depsets).to_list()
package_dir = create_package(ctx, deps_files, ctx.files.nested_packages)
package_dir_depset = depset([package_dir])
result = [
DefaultInfo(
files = package_dir_depset,
runfiles = ctx.runfiles([package_dir]),
),
]
if ctx.attr.package_name:
result.append(LinkablePackageInfo(
package_name = ctx.attr.package_name,
package_path = ctx.attr.package_path,
path = package_dir.path,
files = package_dir_depset,
))
return result
pkg_npm = rule(
implementation = _pkg_npm,
attrs = PKG_NPM_ATTRS,
doc = _DOC,
outputs = PKG_NPM_OUTPUTS,
)
def pkg_npm_macro(name, tgz = None, **kwargs):
"""Wrapper macro around pkg_npm
Args:
name: Unique name for this target
tgz: If provided, creates a `.tar` target that can be used as an action input version of `.pack`
**kwargs: All other args forwarded to pkg_npm
"""
pkg_npm(
name = name,
**kwargs
)
native.alias(
name = name + ".pack",
actual = select({
"@bazel_tools//src/conditions:host_windows": name + ".pack.bat",
"//conditions:default": name + ".pack.sh",
}),
)
native.alias(
name = name + ".publish",
actual = select({
"@bazel_tools//src/conditions:host_windows": name + ".publish.bat",
"//conditions:default": name + ".publish.sh",
}),
)
if tgz != None:
if not tgz.endswith(".tgz"):
fail("tgz output for pkg_npm %s must produce a .tgz file" % name)
native.genrule(
name = "%s.tar" % name,
outs = [tgz],
# NOTE(mattem): on windows, it seems to output a buch of other stuff on stdout when piping, so pipe to tail
# and grab the last line
cmd = "$(location :%s.pack) 2>/dev/null | tail -1 | xargs -I {} cp {} $@" % name,
srcs = [
name,
],
tools = [
":%s.pack" % name,
],
tags = [
"local",
],
visibility = kwargs.get("visibility"),
)