770 lines
27 KiB
TypeScript
770 lines
27 KiB
TypeScript
/// <reference types="node" />
|
|
|
|
import http = require("http");
|
|
import fs = require("fs");
|
|
import path = require("path");
|
|
import url = require("url");
|
|
import URL = url.URL;
|
|
import child_process = require("child_process");
|
|
import os = require("os");
|
|
import crypto = require("crypto");
|
|
|
|
const port = 8888; // harness.ts and webTestResults.html depend on this exact port number.
|
|
const baseUrl = new URL(`http://localhost:8888/`);
|
|
const rootDir = path.dirname(__dirname);
|
|
const useCaseSensitiveFileNames = isFileSystemCaseSensitive();
|
|
|
|
let browser = "IE";
|
|
let grep: string | undefined;
|
|
let verbose = false;
|
|
|
|
interface HttpContent {
|
|
headers: any;
|
|
content: string;
|
|
}
|
|
|
|
namespace HttpContent {
|
|
export function create(headers: object = {}, content?: string) {
|
|
return { headers, content };
|
|
}
|
|
|
|
export function clone(content: HttpContent): HttpContent {
|
|
return content && create(HttpHeaders.clone(content.headers), content.content);
|
|
}
|
|
|
|
export function forMediaType(mediaType: string | string[], content: string): HttpContent {
|
|
return create({ "Content-Type": mediaType, "Content-Length": Buffer.byteLength(content, "utf8") }, content);
|
|
}
|
|
|
|
export function text(content: string): HttpContent {
|
|
return forMediaType("text/plain", content);
|
|
}
|
|
|
|
export function json(content: any): HttpContent {
|
|
return forMediaType("application/json", JSON.stringify(content));
|
|
}
|
|
}
|
|
|
|
namespace HttpHeaders {
|
|
export function clone(headers: http.OutgoingHttpHeaders) {
|
|
return { ...headers };
|
|
}
|
|
|
|
export function getCacheControl(headers: http.IncomingHttpHeaders | http.OutgoingHttpHeaders) {
|
|
let cacheControl = headers["Cache-Control"];
|
|
let noCache = false;
|
|
let noStore = false;
|
|
let maxAge: number = undefined;
|
|
let maxStale: number = undefined;
|
|
let minFresh: number = undefined;
|
|
if (typeof cacheControl === "string") cacheControl = [cacheControl];
|
|
if (Array.isArray(cacheControl)) {
|
|
for (const directive of cacheControl) {
|
|
if (directive === "no-cache") noCache = true;
|
|
else if (directive === "no-store") noStore = true;
|
|
else if (directive === "max-stale") maxStale = Infinity;
|
|
else if (/^no-cache=/.test(directive)) noCache = true;
|
|
else if (/^max-age=/.test(directive)) maxAge = +directive.slice(8).trim();
|
|
else if (/^min-fresh=/.test(directive)) minFresh = +directive.slice(10).trim();
|
|
else if (/^max-stale=/.test(directive)) maxStale = +directive.slice(10).trim();
|
|
}
|
|
}
|
|
return { noCache, noStore, maxAge, maxStale, minFresh };
|
|
}
|
|
|
|
export function getExpires(headers: http.IncomingHttpHeaders | http.OutgoingHttpHeaders) {
|
|
const expires = headers["Expires"];
|
|
if (typeof expires !== "string") return Infinity;
|
|
return new Date(expires).getTime();
|
|
}
|
|
|
|
export function getIfConditions(headers: http.IncomingHttpHeaders): { ifMatch: "*" | string[], ifNoneMatch: "*" | string[], ifModifiedSince: Date, ifUnmodifiedSince: Date } {
|
|
const ifMatch = toMatch(headers["If-Match"]);
|
|
const ifNoneMatch = toMatch(headers["If-None-Match"]);
|
|
const ifModifiedSince = toDate(headers["If-Modified-Since"]);
|
|
const ifUnmodifiedSince = toDate(headers["If-Unmodified-Since"]);
|
|
return { ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince };
|
|
|
|
function toMatch(value: string | string[]) {
|
|
return typeof value === "string" && value !== "*" ? [value] : value;
|
|
}
|
|
|
|
function toDate(value: string | string[]) {
|
|
return value ? new Date(Array.isArray(value) ? value[0] : value) : undefined;
|
|
}
|
|
}
|
|
|
|
export function combine(left: http.OutgoingHttpHeaders, right: http.OutgoingHttpHeaders) {
|
|
return left && right ? { ...left, ...right } :
|
|
left ? { ...left } :
|
|
right ? { ...right } :
|
|
{};
|
|
}
|
|
}
|
|
|
|
interface HttpRequestMessage {
|
|
url: url.URL;
|
|
method: string;
|
|
headers: http.IncomingHttpHeaders;
|
|
content?: HttpContent;
|
|
file?: string;
|
|
stats?: fs.Stats;
|
|
}
|
|
|
|
namespace HttpRequestMessage {
|
|
export function create(method: string, url: URL | string, headers: http.IncomingHttpHeaders, content?: HttpContent) {
|
|
return { method, url: typeof url === "string" ? new URL(url, baseUrl) : url, headers, content };
|
|
}
|
|
|
|
export function getFile(message: HttpRequestMessage) {
|
|
return message.file || (message.file = path.join(rootDir, decodeURIComponent(message.url.pathname)));
|
|
}
|
|
|
|
export function getStats(message: HttpRequestMessage, throwErrors?: boolean) {
|
|
return message.stats || (message.stats = throwErrors ? fs.statSync(getFile(message)) : tryStat(getFile(message)));
|
|
}
|
|
|
|
export function readRequest(req: http.ServerRequest) {
|
|
return new Promise<HttpRequestMessage>((resolve, reject) => {
|
|
let entityData: string | undefined;
|
|
req.setEncoding("utf8");
|
|
req.on("data", (data: string) => {
|
|
if (entityData === undefined) {
|
|
entityData = data;
|
|
}
|
|
else {
|
|
entityData += data;
|
|
}
|
|
});
|
|
req.on("end", () => {
|
|
const content = entityData !== undefined
|
|
? HttpContent.forMediaType(req.headers["Content-Type"], entityData)
|
|
: undefined;
|
|
resolve(HttpRequestMessage.create(req.method, req.url, req.headers, content));
|
|
});
|
|
req.on("error", reject);
|
|
});
|
|
}
|
|
}
|
|
|
|
interface HttpResponseMessage {
|
|
statusCode?: number;
|
|
statusMessage?: string;
|
|
headers: http.OutgoingHttpHeaders;
|
|
content?: HttpContent;
|
|
}
|
|
|
|
namespace HttpResponseMessage {
|
|
export function create(statusCode: number, headers: http.OutgoingHttpHeaders = {}, content?: HttpContent) {
|
|
return { statusCode, headers, content };
|
|
}
|
|
|
|
export function clone(message: HttpResponseMessage): HttpResponseMessage {
|
|
return {
|
|
statusCode: message.statusCode,
|
|
statusMessage: message.statusMessage,
|
|
headers: HttpHeaders.clone(message.headers),
|
|
content: HttpContent.clone(message.content)
|
|
};
|
|
}
|
|
|
|
export function ok(headers: http.OutgoingHttpHeaders, content: HttpContent | undefined): HttpResponseMessage;
|
|
export function ok(content?: HttpContent): HttpResponseMessage;
|
|
export function ok(contentOrHeaders: http.OutgoingHttpHeaders | HttpContent | undefined, content?: HttpContent): HttpResponseMessage {
|
|
let headers: http.OutgoingHttpHeaders;
|
|
if (!content) {
|
|
content = <HttpContent>contentOrHeaders;
|
|
headers = {};
|
|
}
|
|
return create(200, headers, content);
|
|
}
|
|
|
|
export function created(location?: string, etag?: string): HttpResponseMessage {
|
|
return create(201, { "Location": location, "ETag": etag });
|
|
}
|
|
|
|
export function noContent(headers?: http.OutgoingHttpHeaders): HttpResponseMessage {
|
|
return create(204, headers);
|
|
}
|
|
|
|
export function notModified(): HttpResponseMessage {
|
|
return create(304);
|
|
}
|
|
|
|
export function badRequest(): HttpResponseMessage {
|
|
return create(400);
|
|
}
|
|
|
|
export function notFound(): HttpResponseMessage {
|
|
return create(404);
|
|
}
|
|
|
|
export function methodNotAllowed(allowedMethods: string[]): HttpResponseMessage {
|
|
return create(405, { "Allow": allowedMethods });
|
|
}
|
|
|
|
export function preconditionFailed(): HttpResponseMessage {
|
|
return create(412);
|
|
}
|
|
|
|
export function unsupportedMediaType(): HttpResponseMessage {
|
|
return create(415);
|
|
}
|
|
|
|
export function internalServerError(content?: HttpContent): HttpResponseMessage {
|
|
return create(500, {}, content);
|
|
}
|
|
|
|
export function notImplemented(): HttpResponseMessage {
|
|
return create(501);
|
|
}
|
|
|
|
export function setHeaders(obj: HttpResponseMessage | HttpContent, headers: http.OutgoingHttpHeaders) {
|
|
Object.assign(obj.headers, headers);
|
|
}
|
|
|
|
export function writeResponse(message: HttpResponseMessage, response: http.ServerResponse) {
|
|
const content = message.content;
|
|
const headers = HttpHeaders.combine(message.headers, content && content.headers);
|
|
response.writeHead(message.statusCode, message.statusMessage || http.STATUS_CODES[message.statusCode], headers);
|
|
response.end(content && content.content, "utf8");
|
|
}
|
|
}
|
|
|
|
namespace HttpFileMessageHandler {
|
|
function handleGetRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
const file = HttpRequestMessage.getFile(request);
|
|
const stat = HttpRequestMessage.getStats(request);
|
|
const etag = ETag.compute(stat);
|
|
const headers: http.OutgoingHttpHeaders = {
|
|
"Last-Modified": stat.mtime.toUTCString(),
|
|
"ETag": etag
|
|
};
|
|
|
|
let content: HttpContent | undefined;
|
|
if (stat.isFile()) {
|
|
if (request.method === "HEAD") {
|
|
headers["Content-Type"] = guessMediaType(file);
|
|
headers["Content-Length"] = stat.size;
|
|
}
|
|
else {
|
|
content = HttpContent.forMediaType(guessMediaType(file), fs.readFileSync(file, "utf8"));
|
|
}
|
|
}
|
|
else {
|
|
return HttpResponseMessage.notFound();
|
|
}
|
|
|
|
return HttpResponseMessage.ok(headers, content);
|
|
}
|
|
|
|
function handlePutRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
if (request.headers["Content-Encoding"]) return HttpResponseMessage.unsupportedMediaType();
|
|
if (request.headers["Content-Range"]) return HttpResponseMessage.notImplemented();
|
|
|
|
const file = toLocalPath(request.url);
|
|
const exists = fs.existsSync(file);
|
|
mkdir(path.dirname(file));
|
|
fs.writeFileSync(file, request.content, "utf8");
|
|
return exists ? HttpResponseMessage.noContent() : HttpResponseMessage.created();
|
|
}
|
|
|
|
function handleDeleteRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
const file = HttpRequestMessage.getFile(request);
|
|
const stats = HttpRequestMessage.getStats(request);
|
|
if (stats.isFile()) {
|
|
fs.unlinkSync(file);
|
|
}
|
|
else if (stats.isDirectory()) {
|
|
fs.rmdirSync(file);
|
|
}
|
|
|
|
return HttpResponseMessage.noContent();
|
|
}
|
|
|
|
function handleOptionsRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
return HttpResponseMessage.noContent({
|
|
"X-Case-Sensitivity": useCaseSensitiveFileNames ? "CS" : "CI"
|
|
});
|
|
}
|
|
|
|
function handleRequestCore(request: HttpRequestMessage): HttpResponseMessage {
|
|
switch (request.method) {
|
|
case "HEAD":
|
|
case "GET":
|
|
return handleGetRequest(request);
|
|
case "PUT":
|
|
return handlePutRequest(request);
|
|
case "DELETE":
|
|
return handleDeleteRequest(request);
|
|
case "OPTIONS":
|
|
return handleOptionsRequest(request);
|
|
default:
|
|
return HttpResponseMessage.methodNotAllowed(["HEAD", "GET", "PUT", "DELETE", "OPTIONS"]);
|
|
}
|
|
}
|
|
|
|
export function handleRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
let response = HttpCache.get(request);
|
|
if (!response) HttpCache.set(request, response = handleRequestCore(request));
|
|
return response;
|
|
}
|
|
}
|
|
|
|
namespace HttpApiMessageHandler {
|
|
function handleResolveRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
if (!request.content) return HttpResponseMessage.badRequest();
|
|
const localPath = path.resolve(rootDir, request.content.content);
|
|
const relativePath = toURLPath(localPath);
|
|
return relativePath === undefined
|
|
? HttpResponseMessage.badRequest()
|
|
: HttpResponseMessage.ok(HttpContent.text(relativePath));
|
|
}
|
|
|
|
function handleListFilesRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
if (!request.content) return HttpResponseMessage.badRequest();
|
|
const localPath = path.resolve(rootDir, request.content.content);
|
|
const files: string[] = [];
|
|
visit(localPath, files);
|
|
return HttpResponseMessage.ok(HttpContent.json(files));
|
|
|
|
function visit(dirname: string, results: string[]) {
|
|
const { files, directories } = getAccessibleFileSystemEntries(dirname);
|
|
for (const file of files) {
|
|
results.push(toURLPath(path.join(dirname, file)));
|
|
}
|
|
for (const directory of directories) {
|
|
visit(path.join(dirname, directory), results);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleDirectoryExistsRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
if (!request.content) return HttpResponseMessage.badRequest();
|
|
const localPath = path.resolve(rootDir, request.content.content);
|
|
return HttpResponseMessage.ok(HttpContent.json(directoryExists(localPath)));
|
|
}
|
|
|
|
function handlePostRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
switch (request.url.pathname) {
|
|
case "/api/resolve":
|
|
return handleResolveRequest(request);
|
|
case "/api/listFiles":
|
|
return handleListFilesRequest(request);
|
|
case "/api/directoryExists":
|
|
return handleDirectoryExistsRequest(request);
|
|
default:
|
|
return HttpResponseMessage.notFound();
|
|
}
|
|
}
|
|
|
|
export function handleRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
switch (request.method) {
|
|
case "POST":
|
|
return handlePostRequest(request);
|
|
default:
|
|
return HttpResponseMessage.methodNotAllowed(["POST"]);
|
|
}
|
|
}
|
|
|
|
export function match(request: HttpRequestMessage) {
|
|
return /^\/api\//.test(request.url.pathname);
|
|
}
|
|
}
|
|
|
|
namespace HttpMessageHandler {
|
|
export function handleRequest(request: HttpRequestMessage): HttpResponseMessage {
|
|
const { ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince } = HttpHeaders.getIfConditions(request.headers);
|
|
const stats = HttpRequestMessage.getStats(request, /*throwErrors*/ false);
|
|
if (stats) {
|
|
const etag = ETag.compute(stats);
|
|
if (ifNoneMatch) {
|
|
if (ETag.matches(etag, ifNoneMatch)) {
|
|
return HttpResponseMessage.notModified();
|
|
}
|
|
}
|
|
else if (ifModifiedSince && stats.mtime.getTime() <= ifModifiedSince.getTime()) {
|
|
return HttpResponseMessage.notModified();
|
|
}
|
|
|
|
if (ifMatch && !ETag.matches(etag, ifMatch)) {
|
|
return HttpResponseMessage.preconditionFailed();
|
|
}
|
|
|
|
if (ifUnmodifiedSince && stats.mtime.getTime() > ifUnmodifiedSince.getTime()) {
|
|
return HttpResponseMessage.preconditionFailed();
|
|
}
|
|
}
|
|
else if (ifMatch === "*") {
|
|
return HttpResponseMessage.preconditionFailed();
|
|
}
|
|
|
|
if (HttpApiMessageHandler.match(request)) {
|
|
return HttpApiMessageHandler.handleRequest(request);
|
|
}
|
|
else {
|
|
return HttpFileMessageHandler.handleRequest(request);
|
|
}
|
|
}
|
|
|
|
export function handleError(e: any): HttpResponseMessage {
|
|
switch (e.code) {
|
|
case "ENOENT": return HttpResponseMessage.notFound();
|
|
default: return HttpResponseMessage.internalServerError(HttpContent.text(e.toString()));
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace HttpCache {
|
|
interface CacheEntry {
|
|
timestamp: number;
|
|
expires: number;
|
|
response: HttpResponseMessage;
|
|
}
|
|
|
|
const cache: Record<string, CacheEntry> = Object.create(null);
|
|
|
|
export function get(request: HttpRequestMessage) {
|
|
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
|
|
|
|
const cacheControl = HttpHeaders.getCacheControl(request.headers);
|
|
if (cacheControl.noCache) return undefined;
|
|
|
|
const entry = cache[request.url.toString()];
|
|
if (!entry) return undefined;
|
|
|
|
const age = (Date.now() - entry.timestamp) / 1000;
|
|
const lifetime = (entry.expires - Date.now()) / 1000;
|
|
|
|
if (cacheControl.maxAge !== undefined && cacheControl.maxAge < age) return undefined;
|
|
if (lifetime >= 0) {
|
|
if (cacheControl.minFresh !== undefined && cacheControl.minFresh < lifetime) return undefined;
|
|
}
|
|
else {
|
|
if (cacheControl.maxStale === undefined || cacheControl.maxStale < -lifetime) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
if (request.method === "GET" && !entry.response.content) {
|
|
return undefined; // partial response
|
|
}
|
|
|
|
const response = HttpResponseMessage.clone(entry.response);
|
|
response.headers["Age"] = Math.floor(age);
|
|
return response;
|
|
}
|
|
|
|
export function set(request: HttpRequestMessage, response: HttpResponseMessage) {
|
|
if (request.method !== "GET" && request.method !== "HEAD") return response;
|
|
|
|
const cacheControl = HttpHeaders.getCacheControl(request.headers);
|
|
if (cacheControl.noCache) return response;
|
|
if (cacheControl.noStore) return response;
|
|
|
|
const timestamp = Date.now();
|
|
const expires = HttpHeaders.getExpires(response.headers);
|
|
const age = (Date.now() - timestamp) / 1000;
|
|
const lifetime = (expires - Date.now()) / 1000;
|
|
|
|
if (cacheControl.maxAge !== undefined && cacheControl.maxAge < age) return response;
|
|
if (lifetime >= 0) {
|
|
if (cacheControl.minFresh !== undefined && cacheControl.minFresh < lifetime) return response;
|
|
}
|
|
else {
|
|
if (cacheControl.maxStale === undefined || cacheControl.maxStale < -lifetime) return response;
|
|
}
|
|
|
|
cache[request.url.toString()] = {
|
|
timestamp,
|
|
expires,
|
|
response: HttpResponseMessage.clone(response)
|
|
};
|
|
|
|
response.headers["Age"] = Math.floor(age);
|
|
return response;
|
|
}
|
|
|
|
function cleanupCache() {
|
|
for (const url in cache) {
|
|
const entry = cache[url];
|
|
if (entry.expires < Date.now()) delete cache[url];
|
|
}
|
|
}
|
|
|
|
setInterval(cleanupCache, 60000).unref();
|
|
}
|
|
|
|
namespace ETag {
|
|
export function compute(stats: fs.Stats) {
|
|
return JSON.stringify(crypto
|
|
.createHash("sha1")
|
|
.update(JSON.stringify({
|
|
dev: stats.dev,
|
|
ino: stats.ino,
|
|
mtime: stats.mtimeMs,
|
|
size: stats.size
|
|
}))
|
|
.digest("base64"));
|
|
}
|
|
|
|
export function matches(etag: string | undefined, condition: "*" | string[]) {
|
|
return etag && condition === "*" || condition.indexOf(etag) >= 0;
|
|
}
|
|
}
|
|
|
|
function isFileSystemCaseSensitive(): boolean {
|
|
// win32\win64 are case insensitive platforms
|
|
const platform = os.platform();
|
|
if (platform === "win32" || <string>platform === "win64") {
|
|
return false;
|
|
}
|
|
// If this file exists under a different case, we must be case-insensitve.
|
|
return !fs.existsSync(swapCase(__filename));
|
|
}
|
|
|
|
function swapCase(s: string): string {
|
|
return s.replace(/\w/g, (ch) => {
|
|
const up = ch.toUpperCase();
|
|
return ch === up ? ch.toLowerCase() : up;
|
|
});
|
|
}
|
|
|
|
function hasLeadingSeparator(pathname: string) {
|
|
const ch = pathname.charAt(0);
|
|
return ch === "/" || ch === "\\";
|
|
}
|
|
|
|
function ensureLeadingSeparator(pathname: string) {
|
|
return hasLeadingSeparator(pathname) ? pathname : "/" + pathname;
|
|
}
|
|
|
|
function trimLeadingSeparator(pathname: string) {
|
|
return hasLeadingSeparator(pathname) ? pathname.slice(1) : pathname;
|
|
}
|
|
|
|
function normalizeSlashes(path: string) {
|
|
return path.replace(/\\+/g, "/");
|
|
}
|
|
|
|
function hasTrailingSeparator(pathname: string) {
|
|
const ch = pathname.charAt(pathname.length - 1);
|
|
return ch === "/" || ch === "\\";
|
|
}
|
|
|
|
function toLocalPath(url: url.URL) {
|
|
const pathname = decodeURIComponent(url.pathname);
|
|
return path.join(rootDir, pathname);
|
|
}
|
|
|
|
function toURLPath(pathname: string) {
|
|
pathname = normalizeSlashes(pathname);
|
|
pathname = trimLeadingSeparator(pathname);
|
|
|
|
const resolvedPath = path.resolve(rootDir, pathname);
|
|
if (resolvedPath.slice(0, rootDir.length) !== rootDir) {
|
|
return undefined;
|
|
}
|
|
|
|
let relativePath = resolvedPath.slice(rootDir.length);
|
|
relativePath = ensureLeadingSeparator(relativePath);
|
|
relativePath = normalizeSlashes(relativePath);
|
|
return relativePath;
|
|
}
|
|
|
|
function directoryExists(dirname: string) {
|
|
const stat = tryStat(dirname);
|
|
return !!stat && stat.isDirectory();
|
|
}
|
|
|
|
function mkdir(dirname: string) {
|
|
try {
|
|
fs.mkdirSync(dirname);
|
|
}
|
|
catch (e) {
|
|
if (e.code === "EEXIST") {
|
|
return;
|
|
}
|
|
if (e.code === "ENOENT") {
|
|
const parentdir = path.dirname(dirname);
|
|
if (!parentdir || parentdir === dirname) throw e;
|
|
mkdir(parentdir);
|
|
fs.mkdirSync(dirname);
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function tryStat(pathname: string) {
|
|
try {
|
|
return fs.statSync(pathname);
|
|
}
|
|
catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function getAccessibleFileSystemEntries(pathname: string) {
|
|
try {
|
|
const entries = fs.readdirSync(pathname).sort();
|
|
const files: string[] = [];
|
|
const directories: string[] = [];
|
|
for (const entry of entries) {
|
|
// This is necessary because on some file system node fails to exclude
|
|
// "." and "..". See https://github.com/nodejs/node/issues/4002
|
|
if (entry === "." || entry === "..") {
|
|
continue;
|
|
}
|
|
const name = path.join(pathname, entry);
|
|
|
|
let stat: fs.Stats;
|
|
try {
|
|
stat = fs.statSync(name);
|
|
}
|
|
catch (e) {
|
|
continue;
|
|
}
|
|
|
|
if (stat.isFile()) {
|
|
files.push(entry);
|
|
}
|
|
else if (stat.isDirectory()) {
|
|
directories.push(entry);
|
|
}
|
|
}
|
|
return { files, directories };
|
|
}
|
|
catch (e) {
|
|
return { files: [], directories: [] };
|
|
}
|
|
}
|
|
|
|
function log(msg: string) {
|
|
if (verbose) {
|
|
console.log(msg);
|
|
}
|
|
}
|
|
|
|
function guessMediaType(pathname: string) {
|
|
switch (path.extname(pathname).toLowerCase()) {
|
|
case ".html": return "text/html";
|
|
case ".css": return "text/css";
|
|
case ".js": return "application/javascript";
|
|
case ".ts": return "text/plain";
|
|
case ".json": return "text/plain";
|
|
default: return "binary";
|
|
}
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log("Runs an http server on port 8888, looking for tests folder in the current directory\n");
|
|
console.log("Syntax: node webTestServer.js [browser] [tests] [--verbose]\n");
|
|
console.log("Options:");
|
|
console.log(" <browser> The browser to launch. One of 'IE', 'chrome', or 'none' (default 'IE').");
|
|
console.log(" <tests> A regular expression to pass to Mocha.");
|
|
console.log(" --verbose Enables verbose logging.")
|
|
}
|
|
|
|
function parseCommandLine(args: string[]) {
|
|
let offset = 0;
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
const argLower = arg.toLowerCase();
|
|
if (argLower === "--help") {
|
|
printHelp();
|
|
return false;
|
|
}
|
|
else if (argLower === "--verbose") {
|
|
verbose = true;
|
|
}
|
|
else {
|
|
if (offset === 0) {
|
|
browser = arg;
|
|
}
|
|
else if (offset === 1) {
|
|
grep = arg;
|
|
}
|
|
else {
|
|
console.log(`Unrecognized argument: ${arg}\n`);
|
|
return false;
|
|
}
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
if (browser !== "IE" && browser !== "chrome") {
|
|
console.log(`Unrecognized browser '${browser}', expected 'IE' or 'chrome'.`);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function startServer() {
|
|
console.log(`Static file server running at\n => http://localhost:${port}/\nCTRL + C to shutdown`);
|
|
http.createServer((serverRequest: http.ServerRequest, serverResponse: http.ServerResponse) => {
|
|
log(`${serverRequest.method} ${serverRequest.url}`);
|
|
HttpRequestMessage
|
|
.readRequest(serverRequest)
|
|
.then(HttpMessageHandler.handleRequest)
|
|
.catch(HttpMessageHandler.handleError)
|
|
.then(response => HttpResponseMessage.writeResponse(response, serverResponse));
|
|
}).listen(port);
|
|
}
|
|
|
|
function startClient() {
|
|
let browserPath: string;
|
|
if (browser === "none") {
|
|
return;
|
|
}
|
|
|
|
if (browser === "chrome") {
|
|
let defaultChromePath = "";
|
|
switch (os.platform()) {
|
|
case "win32":
|
|
defaultChromePath = "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe";
|
|
break;
|
|
case "darwin":
|
|
defaultChromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
break;
|
|
case "linux":
|
|
defaultChromePath = "/opt/google/chrome/chrome";
|
|
break;
|
|
default:
|
|
console.log(`default Chrome location is unknown for platform '${os.platform()}'`);
|
|
break;
|
|
}
|
|
if (fs.existsSync(defaultChromePath)) {
|
|
browserPath = defaultChromePath;
|
|
}
|
|
else {
|
|
browserPath = browser;
|
|
}
|
|
}
|
|
else {
|
|
const defaultIEPath = "C:/Program Files/Internet Explorer/iexplore.exe";
|
|
if (fs.existsSync(defaultIEPath)) {
|
|
browserPath = defaultIEPath;
|
|
}
|
|
else {
|
|
browserPath = browser;
|
|
}
|
|
}
|
|
|
|
console.log(`Using browser: ${browserPath}`);
|
|
|
|
const queryString = grep ? `?grep=${grep}` : "";
|
|
child_process.spawn(browserPath, [`http://localhost:${port}/tests/webTestResults.html${queryString}`], {
|
|
stdio: "inherit"
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
if (parseCommandLine(process.argv.slice(2))) {
|
|
startServer();
|
|
startClient();
|
|
}
|
|
}
|
|
|
|
main(); |