blob: 3fd9519e0dd32fb2f4d3f1659d95bf6a3075fa8c [file] [log] [blame]
# 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.
"Simple development server"
load("@build_bazel_rules_nodejs//internal/common:sources_aspect.bzl", "sources_aspect")
load(
"@build_bazel_rules_nodejs//internal/js_library:js_library.bzl",
"write_amd_names_shim",
)
load(
"@build_bazel_rules_nodejs//internal/web_package:web_package.bzl",
"html_asset_inject",
)
def _ts_devserver(ctx):
files = depset()
for d in ctx.attr.deps:
if hasattr(d, "node_sources"):
files = depset(transitive = [files, d.node_sources])
elif hasattr(d, "files"):
files = depset(transitive = [files, d.files])
if ctx.label.workspace_root:
# We need the workspace_name for the target being visited.
# Skylark doesn't have this - instead they have a workspace_root
# which looks like "external/repo_name" - so grab the second path segment.
# TODO(alexeagle): investigate a better way to get the workspace name
workspace_name = ctx.label.workspace_root.split("/")[1]
else:
workspace_name = ctx.workspace_name
# Create a manifest file with the sources in arbitrary order, and without
# bazel-bin prefixes ("root-relative paths").
# TODO(alexeagle): we should experiment with keeping the files toposorted, to
# see if we can get performance gains out of the module loader.
ctx.actions.write(ctx.outputs.manifest, "".join([
workspace_name + "/" + f.short_path + "\n"
for f in files.to_list()
]))
amd_names_shim = ctx.actions.declare_file(
"_%s.amd_names_shim.js" % ctx.label.name,
sibling = ctx.outputs.executable,
)
write_amd_names_shim(ctx.actions, amd_names_shim, ctx.attr.bootstrap)
# Requirejs is always needed so its included as the first script
# in script_files before any user specified scripts for the devserver
# to concat in order.
script_files = []
script_files.extend(ctx.files.bootstrap)
script_files.append(ctx.file._requirejs_script)
script_files.append(amd_names_shim)
script_files.extend(ctx.files.scripts)
ctx.actions.write(ctx.outputs.scripts_manifest, "".join([
workspace_name + "/" + f.short_path + "\n"
for f in script_files
]))
devserver_runfiles = [
ctx.executable._devserver,
ctx.outputs.manifest,
ctx.outputs.scripts_manifest,
]
devserver_runfiles += ctx.files.static_files
devserver_runfiles += script_files
if ctx.file.index_html:
injected_index = ctx.actions.declare_file("index.html")
bundle_script = ctx.attr.serving_path
if bundle_script.startswith("/"):
bundle_script = bundle_script[1:]
html_asset_inject(
ctx.file.index_html,
ctx.actions,
ctx.executable._injector,
ctx.attr.additional_root_paths + [
ctx.label.package,
"/".join([ctx.bin_dir.path, ctx.label.package]),
"/".join([ctx.genfiles_dir.path, ctx.label.package]),
],
[f.path for f in ctx.files.static_files] + [bundle_script],
injected_index,
)
devserver_runfiles += [injected_index]
serving_arg = ""
if ctx.attr.serving_path:
serving_arg = "-serving_path=%s" % ctx.attr.serving_path
packages = depset(["/".join([workspace_name, ctx.label.package])] + ctx.attr.additional_root_paths)
# FIXME: more bash dependencies makes Windows support harder
ctx.actions.write(
output = ctx.outputs.executable,
is_executable = True,
content = """#!/bin/sh
RUNFILES="$PWD/.."
{main} {serving_arg} \
-base="$RUNFILES" \
-packages={packages} \
-manifest={workspace}/{manifest} \
-scripts_manifest={workspace}/{scripts_manifest} \
-entry_module={entry_module} \
-port={port} \
"$@"
""".format(
main = ctx.executable._devserver.short_path,
serving_arg = serving_arg,
workspace = workspace_name,
packages = ",".join(packages.to_list()),
manifest = ctx.outputs.manifest.short_path,
scripts_manifest = ctx.outputs.scripts_manifest.short_path,
entry_module = ctx.attr.entry_module,
port = str(ctx.attr.port),
),
)
return [DefaultInfo(
runfiles = ctx.runfiles(
files = devserver_runfiles,
# We don't expect executable targets to depend on the devserver, but if they do,
# they can see the JavaScript code.
transitive_files = depset(ctx.files.data, transitive = [files]),
collect_data = True,
collect_default = True,
),
)]
ts_devserver = rule(
implementation = _ts_devserver,
attrs = {
"additional_root_paths": attr.string_list(
doc = """Additional root paths to serve static_files from.
Paths should include the workspace name such as [\"__main__/resources\"]
""",
),
"bootstrap": attr.label_list(
doc = "Scripts to include in the JS bundle before the module loader (require.js)",
allow_files = [".js"],
),
"data": attr.label_list(
doc = "Dependencies that can be require'd while the server is running",
allow_files = True,
),
"entry_module": attr.string(
doc = """The entry_module should be the AMD module name of the entry module such as `"__main__/src/index"`
ts_devserver concats the following snippet after the bundle to load the application:
`require(["entry_module"]);`
""",
),
"index_html": attr.label(
allow_single_file = True,
doc = """An index.html file, we'll inject the script tag for the bundle,
as well as script tags for .js static_files and link tags for .css
static_files""",
),
"port": attr.int(
doc = """The port that the devserver will listen on.""",
default = 5432,
),
"scripts": attr.label_list(
doc = "User scripts to include in the JS bundle before the application sources",
allow_files = [".js"],
),
"serving_path": attr.string(
# This default repeats the one in the go program. We make it explicit here so we can read it
# when injecting scripts into the index file.
default = "/_/ts_scripts.js",
doc = """The path you can request from the client HTML which serves the JavaScript bundle.
If you don't specify one, the JavaScript can be loaded at /_/ts_scripts.js""",
),
"static_files": attr.label_list(
doc = """Arbitrary files which to be served, such as index.html.
They are served relative to the package where this rule is declared.""",
allow_files = True,
),
"deps": attr.label_list(
doc = "Targets that produce JavaScript, such as `ts_library`",
allow_files = True,
aspects = [sources_aspect],
),
"_devserver": attr.label(
# For local development in rules_typescript, we build the devserver from sources.
# This requires that we have the go toolchain available.
# NB: this value is replaced by "//devserver:server" in the packaged distro
# //devserver:server is the pre-compiled binary.
# That means that our users don't need the go toolchain.
default = Label("//devserver:devserver_bin"),
executable = True,
cfg = "host",
),
"_injector": attr.label(
default = "@build_bazel_rules_nodejs//internal/web_package:injector",
executable = True,
cfg = "host",
),
"_requirejs_script": attr.label(allow_single_file = True, default = Label("@build_bazel_rules_typescript_devserver_deps//node_modules/requirejs:require.js")),
},
outputs = {
"manifest": "%{name}.MF",
"scripts_manifest": "scripts_%{name}.MF",
},
executable = True,
)
"""ts_devserver is a simple development server intended for a quick "getting started" experience.
Additional documentation at https://github.com/alexeagle/angular-bazel-example/wiki/Running-a-devserver-under-Bazel
"""
def ts_devserver_macro(tags = [], **kwargs):
"""ibazel wrapper for `ts_devserver`
This macro re-exposes the `ts_devserver` rule with some extra tags so that
it behaves correctly under ibazel.
This is re-exported in `//:defs.bzl` as `ts_devserver` so if you load the rule
from there, you actually get this macro.
Args:
tags: standard Bazel tags, this macro adds a couple for ibazel
**kwargs: passed through to `ts_devserver`
"""
ts_devserver(
# Users don't need to know that these tags are required to run under ibazel
tags = tags + [
# Tell ibazel not to restart the devserver when its deps change.
"ibazel_notify_changes",
# Tell ibazel to serve the live reload script, since we expect a browser will connect to
# this program.
"ibazel_live_reload",
],
**kwargs
)