-
Notifications
You must be signed in to change notification settings - Fork 519
/
js_library.bzl
430 lines (376 loc) · 18.5 KB
/
js_library.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# 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(
doc = """The package name that the linker will link this js_library 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 js_library 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(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$" or ctx.attr.package_name == "$node_modules_dir$":
# 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,
has_directories = (ctx.attr.package_name == "$node_modules_dir$"),
))
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),
))
if len(typings) or len(typings_depsets) > 1:
# Don't provide DeclarationInfo if there are no typings to provide.
# Improves error messaging downstream if DeclarationInfo is required.
decls = depset(transitive = typings_depsets)
providers.append(declaration_info(
declarations = decls,
deps = ctx.attr.deps,
))
providers.append(OutputGroupInfo(types = decls))
elif ctx.attr.package_name == "$node_modules_dir$":
# If this is directory artifacts npm package then always provide declaration_info
# since we can't scan through files
decls = depset(transitive = files_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$" and package_name != "$node_modules_dir$" else None,
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
**kwargs
)