forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
build: create custom bazel dev-server rule (angular#16937)
Implements a custom bazel dev-server rule that can be exposed eventually. The reason we need a custom dev-server implementation is that the "ts_devserver" is not flexible and needs to be synced into google3 (causing slow syncing; and hestitancy to adding new features. always the question of scope). We need our own implemenation because we want: * Live-reloading to work (bazel-contrib/rules_nodejs#1036) * HTML History API support (currently the ts_devserver always sends a 404 status code) * Better host binding of the server (so that we can access the server on other devices) * Flexibility & control (being able to do changes so that the dev-server fits our needs)
- Loading branch information
1 parent
ce71a45
commit 1c74518
Showing
10 changed files
with
898 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + \ | ||
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: | ||
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) | ||
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, | ||
) |
Oops, something went wrong.