build: replace bazel devserver with shared dev-infra implementation (#45452)

As part of the devtools migration, we copied the custom http server/
dev-server from the `angular/components` repo. This server implementation
has now moved to the shared dev-infra code, and we can clean up the
copy in this repository now.

PR Close #45452
This commit is contained in:
Paul Gschwendtner
2022-03-26 16:54:20 +01:00
committed by Dylan Hunn
parent aebf165359
commit c996b477a9
10 changed files with 6 additions and 428 deletions
@@ -1,7 +1,6 @@
load("//devtools/tools:ng_module.bzl", "ng_module")
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_web")
load("//tools:defaults.bzl", "esbuild")
load("//devtools/tools/dev-server:index.bzl", "dev_server")
load("//tools:defaults.bzl", "esbuild", "http_server")
package(default_visibility = ["//:__subpackages__"])
@@ -51,7 +50,7 @@ pkg_web(
],
)
dev_server(
http_server(
name = "devserver",
srcs = [":dev_app_static_files"],
additional_root_paths = ["angular/devtools/projects/demo-no-zone/src/devapp"],
+2 -3
View File
@@ -1,8 +1,7 @@
load("//devtools/tools:ng_module.bzl", "ng_module")
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library")
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_web")
load("//tools:defaults.bzl", "esbuild")
load("//devtools/tools/dev-server:index.bzl", "dev_server")
load("//tools:defaults.bzl", "esbuild", "http_server")
load("//devtools/tools/esbuild:index.bzl", "LINKER_PROCESSED_FW_PACKAGES")
package(default_visibility = ["//visibility:public"])
@@ -280,7 +279,7 @@ pkg_web(
substitutions = {"BUILD_SCM_COMMIT_SHA": ""},
)
dev_server(
http_server(
name = "devserver",
srcs = [":dev_app_static_files"],
additional_root_paths = ["angular/devtools/src/devapp"],
-37
View File
@@ -1,37 +0,0 @@
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
exports_files(["launcher_template.sh"])
ts_library(
name = "dev-server_lib",
srcs = [
"dev-server.ts",
"ibazel.ts",
"main.ts",
],
# TODO(ESM): remove this once the Bazel NodeJS rules can handle ESM with `nodejs_binary`.
devmode_module = "commonjs",
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",
# TODO(josephperrott): update dependency usages to no longer need bazel patch module resolver
# See: https://github.com/bazelbuild/rules_nodejs/wiki#--bazel_patch_module_resolver-now-defaults-to-false-2324
templated_args = ["--bazel_patch_module_resolver"],
)
-3
View File
@@ -1,3 +0,0 @@
Note: this custom devserver implementation was duped from angular/components.
See: https://github.com/angular/components/tree/master/tools/dev-server
-149
View File
@@ -1,149 +0,0 @@
/**
* @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 browserSync from 'browser-sync';
import {existsSync, readFileSync} from 'fs';
import http from 'http';
import path from 'path';
import 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 {
/** Cached content of the index.html. */
private _index: string|null = null;
/** Instance of the browser-sync server. */
server = browserSync.create();
/** Options of the browser-sync server. */
options: browserSync.Options = {
open: false,
online: false,
port: this.port,
notify: false,
ghostMode: false,
server: {
directory: false,
middleware: [(req, res) => this._bazelMiddleware(req, res)],
},
};
constructor(
readonly port: number,
private _rootPaths: string[],
bindUi: boolean,
private _historyApiFallback: boolean = false,
) {
if (bindUi === false) {
this.options.ui = false;
}
}
/** Starts the server on the given port. */
start() {
return new Promise<void>((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.statusCode = 500;
res.end('Error: No url specified');
return;
}
// Detect if the url escapes the server's root path
for (const rootPath of this._rootPaths) {
const absoluteRootPath = path.resolve(rootPath);
const absoluteJoinedPath = path.resolve(path.posix.join(rootPath, getManifestPath(req.url)));
if (!absoluteJoinedPath.startsWith(absoluteRootPath)) {
res.statusCode = 500;
res.end('Error: Detected directory traversal');
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')) {
res.end(this._getIndex());
} else {
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 {
for (let rootPath of this._rootPaths) {
try {
return require.resolve(path.posix.join(rootPath, getManifestPath(url)));
} catch {
}
}
return null;
}
/** Gets the content of the index.html. */
private _getIndex(): string {
if (!this._index) {
const indexPath = this._resolveUrlFromRunfiles('/index.html');
if (!indexPath) {
throw Error('Could not resolve dev server index.html');
}
// We support specifying a variables.json file next to the index.html which will be inlined
// into the dev app as a `script` tag. It is used to pass in environment-specific variables.
const varsPath = path.join(path.dirname(indexPath), 'variables.json');
const scriptTag = '<script>window.DEV_APP_VARIABLES = ' +
(existsSync(varsPath) ? readFileSync(varsPath, 'utf8') : '{}') + ';</script>';
const content = readFileSync(indexPath, 'utf8');
const headIndex = content.indexOf('</head>');
this._index = content.slice(0, headIndex) + scriptTag + content.slice(headIndex);
}
return this._index;
}
}
/** Gets the manifest path for a given url */
function getManifestPath(url: string) {
// Remove the leading slash from the URL. Manifest paths never
// start with a leading slash.
return url.substring(1);
}
-43
View File
@@ -1,43 +0,0 @@
/**
* @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) {
// If iBazel is not configured for this process, we do not setup the watcher.
if (process.env['IBAZEL_NOTIFY_CHANGES'] !== 'y') {
return;
}
// 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);
});
}
-126
View File
@@ -1,126 +0,0 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "JSNamedModuleInfo")
"""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 + \
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.
for d in ctx.attr.deps:
if JSNamedModuleInfo in d:
files = depset(transitive = [files, d[JSNamedModuleInfo].sources])
elif hasattr(d, "files"):
files = depset(transitive = [files, d.files])
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)
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 = {
"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")
"""),
"deps": attr.label_list(
allow_files = True,
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.
""",
),
"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.
""",
),
"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").
"""),
"_bash_runfile_helpers": attr.label(default = Label("@bazel_tools//tools/bash/runfiles")),
"_dev_server_bin": attr.label(
default = Label("//devtools/tools/dev-server:dev-server_bin"),
),
"_launcher_template": attr.label(allow_single_file = True, default = Label("//devtools/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, port = 4200, tags = [], **kwargs):
dev_server_rule(
name = "%s_launcher" % name,
visibility = ["//visibility:private"],
tags = tags,
testonly = testonly,
**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],
args = ["--port=%s" % port],
testonly = testonly,
)
@@ -1,33 +0,0 @@
#!/bin/bash
# --- begin runfiles.bash initialization v2 ---
# Copy-pasted from the Bazel Bash runfiles library v2. We need to copy the runfile
# helper code as we want to resolve Bazel targets through the runfiles (in order to
# make the dev-server work on windows where runfiles are not symlinked). The runfile
# helpers expose a bash function called "rlocation" that can be used to resolve targets.
set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v2 ---
# If we do not run the devserver as part of a test, we always enforce runfile
# resolution when invoking the devserver NodeJS binary. This is necessary as
# runfile trees are disabled as part of this repository. The devserver NodeJS
# binary would not find a relative runfile tree directory and error out.
if [[ -z "${TEST_SRCDIR:-""}" ]]; then
export RUNFILES_MANIFEST_ONLY="1"
fi
# Resolve the path of the dev-server binary. Note: usually we either need to
# resolve the "nodejs_binary" executable with different file extensions on
# windows, but since we already run this launcher as part of a "sh_binary", we
# can safely execute another shell script from the current shell.
devserverBin=$(rlocation "angular/devtools/tools/dev-server/dev-server_bin.sh")
# Start the devserver with the given arguments. The arguments will be
# substituted based on the rule attributes.
${devserverBin} TEMPLATED_args "$@"
-31
View File
@@ -1,31 +0,0 @@
/**
* @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 minimist from 'minimist';
import {DevServer} from './dev-server';
import {setupBazelWatcherSupport} from './ibazel';
const args = process.argv.slice(2);
const {
root_paths: _rootPathsRaw,
port,
historyApiFallback,
} = minimist(args, {boolean: 'historyApiFallback'});
const rootPaths = _rootPathsRaw ? _rootPathsRaw.split(',') : ['/'];
const bindUi = process.env.TEST_TARGET === undefined;
const server = new DevServer(port, rootPaths, bindUi, historyApiFallback);
// Setup ibazel support.
setupBazelWatcherSupport(server);
// Start the devserver. The server will always bind to the loopback and
// the public interface of the current host.
server.start();
+2
View File
@@ -11,6 +11,7 @@ load("@npm//typescript:index.bzl", "tsc")
load("//packages/bazel:index.bzl", _ng_module = "ng_module", _ng_package = "ng_package")
load("@npm//@angular/dev-infra-private/bazel/benchmark/app_bundling:index.bzl", _app_bundle = "app_bundle")
load("//tools:ng_benchmark.bzl", _ng_benchmark = "ng_benchmark")
load("@npm//@angular/dev-infra-private/bazel/http-server:index.bzl", _http_server = "http_server")
load("@npm//@angular/dev-infra-private/bazel/karma:index.bzl", _karma_web_test = "karma_web_test", _karma_web_test_suite = "karma_web_test_suite")
load("@npm//@angular/dev-infra-private/bazel/api-golden:index.bzl", _api_golden_test = "api_golden_test", _api_golden_test_npm_package = "api_golden_test_npm_package")
load("@npm//@angular/dev-infra-private/bazel:extract_js_module_output.bzl", "extract_js_module_output")
@@ -26,6 +27,7 @@ _INTERNAL_NG_PACKAGE_DEFAULT_ROLLUP = "//packages/bazel/src/ng_package:rollup_fo
esbuild = _esbuild
esbuild_config = _esbuild_config
http_server = _http_server
# Packages which are versioned together on npm
ANGULAR_SCOPED_PACKAGES = ["@angular/%s" % p for p in [