From c996b477a904f14549d59ca63332c4fec2e23f91 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sat, 26 Mar 2022 16:54:20 +0100 Subject: [PATCH] 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 --- .../projects/demo-no-zone/src/BUILD.bazel | 5 +- devtools/src/BUILD.bazel | 5 +- devtools/tools/dev-server/BUILD.bazel | 37 ----- devtools/tools/dev-server/README.md | 3 - devtools/tools/dev-server/dev-server.ts | 149 ------------------ devtools/tools/dev-server/ibazel.ts | 43 ----- devtools/tools/dev-server/index.bzl | 126 --------------- .../tools/dev-server/launcher_template.sh | 33 ---- devtools/tools/dev-server/main.ts | 31 ---- tools/defaults.bzl | 2 + 10 files changed, 6 insertions(+), 428 deletions(-) delete mode 100644 devtools/tools/dev-server/BUILD.bazel delete mode 100644 devtools/tools/dev-server/README.md delete mode 100644 devtools/tools/dev-server/dev-server.ts delete mode 100644 devtools/tools/dev-server/ibazel.ts delete mode 100644 devtools/tools/dev-server/index.bzl delete mode 100644 devtools/tools/dev-server/launcher_template.sh delete mode 100644 devtools/tools/dev-server/main.ts diff --git a/devtools/projects/demo-no-zone/src/BUILD.bazel b/devtools/projects/demo-no-zone/src/BUILD.bazel index 14c52495c6f..5da2b87c417 100644 --- a/devtools/projects/demo-no-zone/src/BUILD.bazel +++ b/devtools/projects/demo-no-zone/src/BUILD.bazel @@ -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"], diff --git a/devtools/src/BUILD.bazel b/devtools/src/BUILD.bazel index 7198e939843..05cfbc50675 100644 --- a/devtools/src/BUILD.bazel +++ b/devtools/src/BUILD.bazel @@ -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"], diff --git a/devtools/tools/dev-server/BUILD.bazel b/devtools/tools/dev-server/BUILD.bazel deleted file mode 100644 index 7554504f028..00000000000 --- a/devtools/tools/dev-server/BUILD.bazel +++ /dev/null @@ -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"], -) diff --git a/devtools/tools/dev-server/README.md b/devtools/tools/dev-server/README.md deleted file mode 100644 index 1327e42270c..00000000000 --- a/devtools/tools/dev-server/README.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/devtools/tools/dev-server/dev-server.ts b/devtools/tools/dev-server/dev-server.ts deleted file mode 100644 index 7b187990628..00000000000 --- a/devtools/tools/dev-server/dev-server.ts +++ /dev/null @@ -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((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 = ''; - const content = readFileSync(indexPath, 'utf8'); - const headIndex = content.indexOf(''); - 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); -} diff --git a/devtools/tools/dev-server/ibazel.ts b/devtools/tools/dev-server/ibazel.ts deleted file mode 100644 index 1f3b8337085..00000000000 --- a/devtools/tools/dev-server/ibazel.ts +++ /dev/null @@ -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); - }); -} diff --git a/devtools/tools/dev-server/index.bzl b/devtools/tools/dev-server/index.bzl deleted file mode 100644 index fec0380ece1..00000000000 --- a/devtools/tools/dev-server/index.bzl +++ /dev/null @@ -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, - ) diff --git a/devtools/tools/dev-server/launcher_template.sh b/devtools/tools/dev-server/launcher_template.sh deleted file mode 100644 index b14175c3de4..00000000000 --- a/devtools/tools/dev-server/launcher_template.sh +++ /dev/null @@ -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 "$@" diff --git a/devtools/tools/dev-server/main.ts b/devtools/tools/dev-server/main.ts deleted file mode 100644 index d0a18b335ea..00000000000 --- a/devtools/tools/dev-server/main.ts +++ /dev/null @@ -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(); diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 4cccc2b918a..9eb986ea4a0 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -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 [