blob: fea085a49276ad9450fe376acd3f1ba0df260ca7 [file]
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"js_library can be used to expose and share any library package."
load(
"//:providers.bzl",
"DeclarationInfo",
"ExternalNpmPackageInfo",
"JSEcmaScriptModuleInfo",
"JSModuleInfo",
"JSNamedModuleInfo",
"LinkablePackageInfo",
"declaration_info",
"js_ecma_script_module_info",
"js_module_info",
"js_named_module_info",
)
load(
"//third_party/github.com/bazelbuild/bazel-skylib:rules/private/copy_file_private.bzl",
"copy_bash",
"copy_cmd",
)
_ATTRS = {
"amd_names": attr.string_dict(
doc = """Non-public legacy API, not recommended to make new usages.
See documentation on AmdNamesInfo""",
),
"deps": attr.label_list(),
"is_windows": attr.bool(
doc = "Internal use only. Automatically set by macro",
mandatory = True,
),
# module_name for legacy ts_library module_mapping support
# which is still being used in a couple of tests
# TODO: remove once legacy module_mapping is removed
"module_name": attr.string(
doc = "Internal use only. It will be removed soon.",
),
"named_module_srcs": attr.label_list(
doc = """Non-public legacy API, not recommended to make new usages.
A subset of srcs that are javascript named-UMD or
named-AMD for use in rules such as concatjs_devserver.
They will be copied into the package bin folder if needed.""",
allow_files = True,
),
"package_name": attr.string(),
"package_path": attr.string(),
"srcs": attr.label_list(allow_files = True),
"strip_prefix": attr.string(
doc = "Path components to strip from the start of the package import path",
default = "",
),
}
AmdNamesInfo = provider(
doc = "Non-public API. Provides access to the amd_names attribute of js_library",
fields = {"names": """Mapping from require module names to global variables.
This allows devmode JS sources to load unnamed UMD bundles from third-party libraries."""},
)
def write_amd_names_shim(actions, amd_names_shim, targets):
"""Shim AMD names for UMD bundles that were shipped anonymous.
These are collected from our bootstrap deps (the only place global scripts should appear)
Args:
actions: starlark rule execution context.actions
amd_names_shim: File where the shim is written
targets: dependencies to be scanned for AmdNamesInfo providers
"""
amd_names_shim_content = """// GENERATED by js_library.bzl
// Shim these global symbols which were defined by a bootstrap script
// so that they can be loaded with named require statements.
"""
for t in targets:
if AmdNamesInfo in t:
for n in t[AmdNamesInfo].names.items():
amd_names_shim_content += "define(\"%s\", function() { return %s });\n" % n
actions.write(amd_names_shim, amd_names_shim_content)
def _link_path(ctx, all_files):
link_path = "/".join([p for p in [ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package] if p])
# Strip a prefix from the package require path
if ctx.attr.strip_prefix:
link_path += "/" + ctx.attr.strip_prefix
# Check that strip_prefix contains at least one src path
check_prefix = "/".join([p for p in [ctx.label.package, ctx.attr.strip_prefix] if p])
prefix_contains_src = False
for file in all_files:
if file.short_path.startswith(check_prefix):
prefix_contains_src = True
break
if not prefix_contains_src:
fail("js_library %s strip_prefix path does not contain any of the provided sources" % ctx.label)
return link_path
def _impl(ctx):
input_files = ctx.files.srcs + ctx.files.named_module_srcs
all_files = []
typings = []
js_files = []
named_module_files = []
for idx, f in enumerate(input_files):
file = f
# copy files into bin if needed
if file.is_source and not file.path.startswith("external/"):
dst = ctx.actions.declare_file(file.basename, sibling = file)
if ctx.attr.is_windows:
copy_cmd(ctx, file, dst)
else:
copy_bash(ctx, file, dst)
# re-assign file to the one now copied into the bin folder
file = dst
# register js files
if file.basename.endswith(".js") or file.basename.endswith(".js.map") or file.basename.endswith(".json"):
js_files.append(file)
# register typings
if (
(
file.path.endswith(".d.ts") or
file.path.endswith(".d.ts.map") or
# package.json may be required to resolve "typings" key
file.path.endswith("/package.json")
) and
# exclude eg. external/npm/node_modules/protobufjs/node_modules/@types/node/index.d.ts
# these would be duplicates of the typings provided directly in another dependency.
# also exclude all /node_modules/typescript/lib/lib.*.d.ts files as these are determined by
# the tsconfig "lib" attribute
len(file.path.split("/node_modules/")) < 3 and file.path.find("/node_modules/typescript/lib/lib.") == -1
):
typings.append(file)
# ctx.files.named_module_srcs are merged after ctx.files.srcs
if idx >= len(ctx.files.srcs):
named_module_files.append(file)
# every single file on bin should be added here
all_files.append(file)
files_depset = depset(all_files)
js_files_depset = depset(js_files)
named_module_files_depset = depset(named_module_files)
typings_depset = depset(typings)
files_depsets = [files_depset]
npm_sources_depsets = [files_depset]
direct_ecma_script_module_depsets = [files_depset]
direct_sources_depsets = [files_depset]
direct_named_module_sources_depsets = [named_module_files_depset]
typings_depsets = [typings_depset]
js_files_depsets = [js_files_depset]
for dep in ctx.attr.deps:
if ExternalNpmPackageInfo in dep:
npm_sources_depsets.append(dep[ExternalNpmPackageInfo].sources)
else:
if JSEcmaScriptModuleInfo in dep:
direct_ecma_script_module_depsets.append(dep[JSEcmaScriptModuleInfo].direct_sources)
direct_sources_depsets.append(dep[JSEcmaScriptModuleInfo].direct_sources)
if JSModuleInfo in dep:
js_files_depsets.append(dep[JSModuleInfo].direct_sources)
direct_sources_depsets.append(dep[JSModuleInfo].direct_sources)
if JSNamedModuleInfo in dep:
direct_named_module_sources_depsets.append(dep[JSNamedModuleInfo].direct_sources)
direct_sources_depsets.append(dep[JSNamedModuleInfo].direct_sources)
if DeclarationInfo in dep:
typings_depsets.append(dep[DeclarationInfo].declarations)
direct_sources_depsets.append(dep[DeclarationInfo].declarations)
if DefaultInfo in dep:
files_depsets.append(dep[DefaultInfo].files)
providers = [
DefaultInfo(
files = depset(transitive = files_depsets),
runfiles = ctx.runfiles(
files = all_files,
transitive_files = depset(
transitive = files_depsets + typings_depsets,
),
),
),
AmdNamesInfo(names = ctx.attr.amd_names),
js_ecma_script_module_info(
sources = depset(transitive = direct_ecma_script_module_depsets),
deps = ctx.attr.deps,
),
js_module_info(
sources = depset(transitive = js_files_depsets),
deps = ctx.attr.deps,
),
js_named_module_info(
sources = depset(transitive = direct_named_module_sources_depsets),
deps = ctx.attr.deps,
),
]
if ctx.attr.package_name == "$node_modules$":
# special case for external npm deps
workspace_name = ctx.label.workspace_name if ctx.label.workspace_name else ctx.workspace_name
providers.append(ExternalNpmPackageInfo(
direct_sources = depset(transitive = direct_sources_depsets),
sources = depset(transitive = npm_sources_depsets),
workspace = workspace_name,
path = ctx.attr.package_path,
))
else:
providers.append(LinkablePackageInfo(
package_name = ctx.attr.package_name,
package_path = ctx.attr.package_path,
path = _link_path(ctx, all_files),
files = depset(transitive = direct_sources_depsets),
))
# Don't provide DeclarationInfo if there are no typings to provide.
# Improves error messaging downstream if DeclarationInfo is required.
if len(typings) or len(typings_depsets) > 1:
decls = depset(transitive = typings_depsets)
providers.append(declaration_info(
declarations = decls,
deps = ctx.attr.deps,
))
providers.append(OutputGroupInfo(types = decls))
return providers
_js_library = rule(
implementation = _impl,
attrs = _ATTRS,
)
def js_library(
name,
srcs = [],
package_name = None,
package_path = "",
deps = [],
**kwargs):
"""Groups JavaScript code so that it can be depended on like an npm package.
`js_library` is intended to be used internally within Bazel, such as between two libraries in your monorepo.
This rule doesn't perform any build steps ("actions") so it is similar to a `filegroup`.
However it provides several Bazel "Providers" for interop with other rules.
> Compare this to `pkg_npm` which just produces a directory output, and therefore can't expose individual
> files to downstream targets and causes a cascading re-build of all transitive dependencies when any file
> changes. Also `pkg_npm` is intended to publish your code for external usage outside of Bazel, like
> by publishing to npm or artifactory, while `js_library` is for internal dependencies within your repo.
`js_library` also copies any source files into the bazel-out folder.
This is the same behavior as the `copy_to_bin` rule.
By copying the complete package to the output tree, we ensure that the linker (our `npm link` equivalent)
will make your source files available in the node_modules tree where resolvers expect them.
It also means you can have relative imports between the files
rather than being forced to use Bazel's "Runfiles" semantics where any program might need a helper library
to resolve files between the logical union of the source tree and the output tree.
### Example
A typical example usage of `js_library` is to expose some sources with a package name:
```python
ts_project(
name = "compile_ts",
srcs = glob(["*.ts"]),
)
js_library(
name = "my_pkg",
# Code that depends on this target can import from "@myco/mypkg"
package_name = "@myco/mypkg",
# Consumers might need fields like "main" or "typings"
srcs = ["package.json"],
# The .js and .d.ts outputs from above will be part of the package
deps = [":compile_ts"],
)
```
> To help work with "named AMD" modules as required by `concatjs_devserver` and other Google-style "concatjs" rules,
> `js_library` has some undocumented advanced features you can find in the source code or in our examples.
> These should not be considered a public API and aren't subject to our usual support and semver guarantees.
### Outputs
Like all Bazel rules it produces a default output by providing [DefaultInfo].
You'll get these outputs if you include this in the `srcs` of a typical rule like `filegroup`,
and these will be the printed result when you `bazel build //some:js_library_target`.
The default outputs are all of:
- [DefaultInfo] produced by targets in `deps`
- A copy of all sources (InputArtifacts from your source tree) in the bazel-out directory
When there are TypeScript typings files, `js_library` provides [DeclarationInfo](#declarationinfo)
so this target can be a dependency of a TypeScript rule. This includes any `.d.ts` files in `srcs` as well
as transitive ones from `deps`.
It will also provide [OutputGroupInfo] with a "types" field, so you can select the typings outputs with
`bazel build //some:js_library_target --output_groups=types` or with a `filegroup` rule using the
[output_group] attribute.
In order to work with the linker (similar to `npm link` for first-party monorepo deps), `js_library` provides
[LinkablePackageInfo](#linkablepackageinfo) for use with our "linker" that makes this package importable.
It also provides:
- [ExternalNpmPackageInfo](#externalnpmpackageinfo) to interop with rules that expect third-party npm packages.
- [JSModuleInfo](#jsmoduleinfo) so rules like bundlers can collect the transitive set of .js files
- [JSNamedModuleInfo](#jsnamedmoduleinfo) for rules that expect named AMD or `goog.module` format JS
[OutputGroupInfo]: https://docs.bazel.build/versions/master/skylark/lib/OutputGroupInfo.html
[DefaultInfo]: https://docs.bazel.build/versions/master/skylark/lib/DefaultInfo.html
[output_group]: https://docs.bazel.build/versions/master/be/general.html#filegroup.output_group
Args:
name: The name for the target
srcs: The list of files that comprise the package
package_name: The name it will be imported by. Should match the "name" field in the package.json file.
If package_name == "$node_modules$" this indictates that this js_library target is one or more external npm
packages in node_modules. This is a special case that used be covered by the internal only
`external_npm_package` attribute. NB: '$' is an illegal character
for npm packages names so this reserved name will not conflict with any valid package_name values
This is used by the yarn_install & npm_install repository rules for npm dependencies installed by
yarn & npm. When true, js_library will provide ExternalNpmPackageInfo.
It can also be used for user-managed npm dependencies if node_modules is layed out outside of bazel.
For example,
```starlark
js_library(
name = "node_modules",
srcs = glob(
include = [
"node_modules/**/*.js",
"node_modules/**/*.d.ts",
"node_modules/**/*.json",
"node_modules/.bin/*",
],
exclude = [
# Files under test & docs may contain file names that
# are not legal Bazel labels (e.g.,
# node_modules/ecstatic/test/public/中文/檔案.html)
"node_modules/**/test/**",
"node_modules/**/docs/**",
# Files with spaces in the name are not legal Bazel labels
"node_modules/**/* */**",
"node_modules/**/* *",
],
),
# Special value to provide ExternalNpmPackageInfo which is used by downstream
# rules that use these npm dependencies
package_name = "$node_modules$",
)
```
See `examples/user_managed_deps` for a working example of user-managed npm dependencies.
package_path: The directory in the workspace to link to.
If set, link this js_library to the node_modules under the package path specified.
If unset, the default is to link to the node_modules root of the workspace.
deps: Other targets that provide JavaScript code
**kwargs: Other attributes
"""
# Undocumented features
amd_names = kwargs.pop("amd_names", {})
module_name = kwargs.pop("module_name", None)
named_module_srcs = kwargs.pop("named_module_srcs", [])
if module_name:
fail("use package_name instead of module_name in target //%s:%s" % (native.package_name(), name))
if kwargs.pop("is_windows", None):
fail("is_windows is set by the js_library macro and should not be set explicitly")
_js_library(
name = name,
amd_names = amd_names,
srcs = srcs,
named_module_srcs = named_module_srcs,
deps = deps,
package_name = package_name,
package_path = package_path,
# module_name for legacy ts_library module_mapping support
# which is still being used in a couple of tests
# TODO: remove once legacy module_mapping is removed
module_name = package_name if package_name != "$node_modules$" else None,
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
**kwargs
)