mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Dylan Hunn
parent
aebf165359
commit
c996b477a9
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -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();
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user