mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Previously devtools used a nested workspace for its bazel configurations. This meant framework dependencies were consumed via npm. Now devtools is part of the root bazel directory that all other files in this codebase fall under. This allows us to build devtools using local angular packages, removing the need to consume these dependencies with npn. This is useful because we no longer have to update these dependencies with an automated tool like renovate, and our CI tests will always run against the most up to date framework packages.
149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|