Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: create custom bazel dev-server rule #16937

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@firebase/app-types": "^0.3.2",
"@octokit/rest": "^16.28.7",
"@schematics/angular": "^8.2.1",
"@types/browser-sync": "^2.26.1",
"@types/chalk": "^0.4.31",
"@types/fs-extra": "^4.0.3",
"@types/glob": "^5.0.33",
Expand All @@ -90,8 +91,10 @@
"@types/node": "^7.0.21",
"@types/parse5": "^5.0.0",
"@types/run-sequence": "^0.0.29",
"@types/send": "^0.14.5",
"autoprefixer": "^6.7.6",
"axe-webdriverjs": "^1.1.1",
"browser-sync": "^2.26.7",
"chalk": "^1.1.3",
"clang-format": "^1.2.4",
"codelyzer": "^5.1.0",
Expand Down Expand Up @@ -141,6 +144,7 @@
"run-sequence": "^1.2.2",
"scss-bundle": "^2.0.1-beta.7",
"selenium-webdriver": "^3.6.0",
"send": "^0.17.1",
"shelljs": "^0.8.3",
"sorcery": "^0.10.0",
"stylelint": "^10.1.0",
Expand Down
19 changes: 9 additions & 10 deletions src/dev-app/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package(default_visibility = ["//visibility:public"])

load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
load("@npm_bazel_typescript//:defs.bzl", "ts_devserver")
load("//:packages.bzl", "MATERIAL_EXPERIMENTAL_SCSS_LIBS")
load("//tools:defaults.bzl", "ng_module")
load("//tools/dev-server:index.bzl", "dev_server")

ng_module(
name = "dev-app",
Expand Down Expand Up @@ -90,13 +90,12 @@ sass_binary(
] + MATERIAL_EXPERIMENTAL_SCSS_LIBS,
)

ts_devserver(
dev_server(
name = "devserver",
additional_root_paths = [
"npm/node_modules",
],
port = 4200,
static_files = [
srcs = [
"index.html",
"system-config.js",
"system-rxjs-operators.js",
":theme",
"//src/dev-app/icon:icon_demo_assets",
"@npm//:node_modules/@material/animation/dist/mdc.animation.js",
Expand Down Expand Up @@ -137,9 +136,9 @@ ts_devserver(
"@npm//:node_modules/systemjs/dist/system.js",
"@npm//:node_modules/tslib/tslib.js",
"@npm//:node_modules/zone.js/dist/zone.js",
"index.html",
"system-config.js",
"system-rxjs-operators.js",
],
additional_root_paths = [
"npm/node_modules",
],
deps = [
":dev-app",
Expand Down
6 changes: 0 additions & 6 deletions src/dev-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@
<body>
<dev-app>Loading...</dev-app>
</body>
<!--
Sets up the live reloading script from ibazel if present. This is a workaround
and will not work if the port changes (in case it is already used).
TODO(devversion): replace once https://github.com/bazelbuild/rules_nodejs/issues/1036 is fixed.
-->
<script src="http://localhost:35729/livereload.js?snipver=1" async></script>
<script src="core-js/client/core.js"></script>
<script src="zone.js/dist/zone.js"></script>
<script src="hammerjs/hammer.min.js"></script>
Expand Down
32 changes: 32 additions & 0 deletions tools/dev-server/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])

load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
load("//tools:defaults.bzl", "ts_library")

exports_files(["launcher_template.sh"])

ts_library(
name = "dev-server_lib",
srcs = [
"dev-server.ts",
"ibazel.ts",
"main.ts",
],
deps = [
"@npm//@types/browser-sync",
"@npm//@types/minimist",
"@npm//@types/node",
"@npm//@types/send",
"@npm//browser-sync",
"@npm//minimist",
"@npm//send",
],
)

nodejs_binary(
name = "dev-server_bin",
data = [
":dev-server_lib",
],
entry_point = ":main.ts",
)
97 changes: 97 additions & 0 deletions tools/dev-server/dev-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as browserSync from 'browser-sync';
import * as http from 'http';
import * as path from 'path';
import * as send from 'send';

/**
* Dev Server implementation that uses browser-sync internally. This dev server
* supports Bazel runfile resolution in order to make it work in a Bazel sandbox
* environment and on Windows (with a runfile manifest file).
*/
export class DevServer {
/** Instance of the browser-sync server. */
server = browserSync.create();

/** Options of the browser-sync server. */
options: browserSync.Options = {
open: false,
port: this.port,
notify: false,
ghostMode: false,
server: true,
middleware: (req, res) => this._bazelMiddleware(req, res),
};

constructor(
readonly port: number, private _rootPaths: string[],
private _historyApiFallback: boolean = false) {}

/** Starts the server on the given port. */
async start() {
return new Promise((resolve, reject) => {
this.server.init(this.options, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

/** Reloads all browsers that currently visit a page from the server. */
reload() {
this.server.reload();
}

/**
* Middleware function used by BrowserSync. This function is responsible for
* Bazel runfile resolution and HTML History API support.
*/
private _bazelMiddleware(req: http.IncomingMessage, res: http.ServerResponse) {
if (!req.url) {
res.end('No url specified. Error');
return;
}

// Implements the HTML history API fallback logic based on the requirements of the
// "connect-history-api-fallback" package. See the conditions for a request being redirected
// to the index: https://github.com/bripkens/connect-history-api-fallback#introduction
if (this._historyApiFallback && req.method === 'GET' && !req.url.includes('.') &&
req.headers.accept && req.headers.accept.includes('text/html')) {
req.url = '/index.html';
}

const resolvedPath = this._resolveUrlFromRunfiles(req.url);

if (resolvedPath === null) {
res.statusCode = 404;
res.end('Page not found');
return;
}

send(req, resolvedPath).pipe(res);
}

/** Resolves a given URL from the runfiles using the corresponding manifest path. */
private _resolveUrlFromRunfiles(url: string): string|null {
// Remove the leading slash from the URL. Manifest paths never
// start with a leading slash.
const manifestPath = url.substring(1);
for (let rootPath of this._rootPaths) {
try {
return require.resolve(path.posix.join(rootPath, manifestPath));
} catch {
}
}
return null;
}
}
37 changes: 37 additions & 0 deletions tools/dev-server/ibazel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {createInterface} from 'readline';
import {DevServer} from './dev-server';

// ibazel will write this string after a successful build.
const ibazelNotifySuccessMessage = 'IBAZEL_BUILD_COMPLETED SUCCESS';

/**
* Sets up ibazel support for the specified devserver. ibazel communicates with
* an executable over the "stdin" interface. Whenever a specific message is sent
* over "stdin", the devserver can be reloaded.
*/
export function setupBazelWatcherSupport(server: DevServer) {
// ibazel communicates via the stdin interface.
const rl = createInterface({input: process.stdin, terminal: false});

rl.on('line', (chunk: string) => {
if (chunk === ibazelNotifySuccessMessage) {
server.reload();
}
});

rl.on('close', () => {
// Give ibazel 5s to kill this process, otherwise we exit the process manually.
setTimeout(() => {
console.error('ibazel failed to stop the devserver after 5s.');
process.exit(1);
}, 5000);
});
}
134 changes: 134 additions & 0 deletions tools/dev-server/index.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
load("@build_bazel_rules_nodejs//internal/common:sources_aspect.bzl", "sources_aspect")

"""Gets the workspace name of the given rule context."""

def _get_workspace_name(ctx):
if ctx.label.workspace_root:
# We need the workspace_name for the target being visited.
# Starlark doesn't have this - instead they have a workspace_root
# which looks like "external/repo_name" - so grab the second path segment.
return ctx.label.workspace_root.split("/")[1]
else:
return ctx.workspace_name

"""Implementation of the dev server rule."""

def _dev_server_rule_impl(ctx):
files = depset(ctx.files.srcs)

# List of files which are required for the devserver to run. This includes the
# bazel runfile helpers (to resolve runfiles in bash) and the devserver binary
# with its transitive runfiles (in order to be able to run the devserver).
required_tools = ctx.files._bash_runfile_helpers + \
devversion marked this conversation as resolved.
Show resolved Hide resolved
devversion marked this conversation as resolved.
Show resolved Hide resolved
ctx.files._dev_server_bin + \
ctx.attr._dev_server_bin[DefaultInfo].files.to_list() + \
ctx.attr._dev_server_bin[DefaultInfo].data_runfiles.files.to_list()

# Walk through all dependencies specified in the "deps" attribute. These labels need to be
# unwrapped in case there are built using TypeScript-specific rules. This is because targets
# built using "ts_library" or "ng_module" do not declare the generated JS files as default
# rule output. The output aspect that is applied to the "deps" attribute, provides two struct
# fields which resolve to the unwrapped JS output files.
# https://github.com/bazelbuild/rules_nodejs/blob/e04c8c31f3cb859754ea5c5e97f331a3932b725d/internal/common/sources_aspect.bzl#L53-L55
for d in ctx.attr.deps:
devversion marked this conversation as resolved.
Show resolved Hide resolved
if hasattr(d, "node_sources"):
files = depset(transitive = [files, d.node_sources])
elif hasattr(d, "files"):
files = depset(transitive = [files, d.files])
if hasattr(d, "dev_scripts"):
files = depset(transitive = [files, d.dev_scripts])

workspace_name = _get_workspace_name(ctx)
root_paths = ["", "/".join([workspace_name, ctx.label.package])] + ctx.attr.additional_root_paths

# We can't use "ctx.actions.args()" because there is no way to convert the args object
# into a string representing the command line arguments. It looks like bazel has some
# internal logic to compute the string representation of "ctx.actions.args()".
args = '--root_paths="%s" ' % ",".join(root_paths)
devversion marked this conversation as resolved.
Show resolved Hide resolved
args += "--port=%s " % ctx.attr.port

if ctx.attr.historyApiFallback:
args += "--historyApiFallback "

ctx.actions.expand_template(
template = ctx.file._launcher_template,
output = ctx.outputs.launcher,
substitutions = {
"TEMPLATED_args": args,
},
is_executable = True,
)

return [
DefaultInfo(runfiles = ctx.runfiles(
files = files.to_list() + required_tools,
collect_data = True,
collect_default = True,
)),
]

dev_server_rule = rule(
implementation = _dev_server_rule_impl,
outputs = {
"launcher": "%{name}.sh",
},
attrs = {
"srcs": attr.label_list(allow_files = True, doc = """
Sources that should be available to the dev-server. This attribute can be
used for explicit files. This attribute only uses the files exposed by the
DefaultInfo provider (i.e. TypeScript targets should be added to "deps").
"""),
"additional_root_paths": attr.string_list(doc = """
Additionally paths to serve files from. The paths should be formatted
as manifest paths (e.g. "my_workspace/src")
"""),
"historyApiFallback": attr.bool(
default = True,
doc = """
Whether the devserver should fallback to "/index.html" for non-file requests.
This is helpful for single page applications using the HTML history API.
""",
),
"port": attr.int(
default = 4200,
doc = """The port that the devserver will listen on.""",
),
"deps": attr.label_list(
allow_files = True,
aspects = [sources_aspect],
doc = """
Dependencies that need to be available to the dev-server. This attribute can be
used for TypeScript targets which provide multiple flavors of output.
""",
),
"_bash_runfile_helpers": attr.label(default = Label("@bazel_tools//tools/bash/runfiles")),
"_dev_server_bin": attr.label(
default = Label("//tools/dev-server:dev-server_bin"),
),
"_launcher_template": attr.label(allow_single_file = True, default = Label("//tools/dev-server:launcher_template.sh")),
},
)

"""
Creates a dev server that can depend on individual bazel targets. The server uses
bazel runfile resolution in order to work with Bazel package paths. e.g. developers can
request files through their manifest path: "my_workspace/src/dev-app/my-genfile".
"""

def dev_server(name, testonly = False, tags = [], **kwargs):
dev_server_rule(
name = "%s_launcher" % name,
visibility = ["//visibility:private"],
tags = tags,
**kwargs
)

native.sh_binary(
name = name,
# The "ibazel_notify_changes" tag tells ibazel to not relaunch the executable on file
# changes. Rather it will communicate with the server implementation through "stdin".
tags = tags + ["ibazel_notify_changes"],
srcs = ["%s_launcher.sh" % name],
data = [":%s_launcher" % name],
testonly = testonly,
)
Loading