-
Notifications
You must be signed in to change notification settings - Fork 6.8k
build: create custom bazel dev-server rule #16937
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
Merged
jelbourn
merged 1 commit into
angular:master
from
devversion:build/implement-custom-devserver
Sep 3, 2019
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 + \ | ||
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, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.