diff --git a/Jakefile.js b/Jakefile.js
index 49641cab710..621111aa7f7 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -95,6 +95,7 @@ var harnessCoreSources = [
"harness.ts",
"collections.ts",
"vpath.ts",
+ "events.ts",
"vfs.ts",
"virtualFileSystemWithWatch.ts",
"sourceMapRecorder.ts",
diff --git a/src/harness/collections.ts b/src/harness/collections.ts
index e0f5e9a2aea..7d661cdeacc 100644
--- a/src/harness/collections.ts
+++ b/src/harness/collections.ts
@@ -1,13 +1,23 @@
///
-namespace Collections {
- import compareValues = ts.compareValues;
+namespace collections {
+ // NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been added
+ // to reduce the number of direct dependencies on compiler and services to eventually break away
+ // from depending directly on the compiler to speed up compilation time.
+
import binarySearch = ts.binarySearch;
import removeAt = ts.orderedRemoveItemAt;
+ export function compareValues(a: T, b: T): number {
+ if (a === b) return 0;
+ if (a === undefined) return -1;
+ if (b === undefined) return +1;
+ return a < b ? -1 : +1;
+ }
+
const caseInsensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "accent" }) : undefined;
const caseSensitiveComparisonCollator = typeof Intl === "object" ? new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "variant" }) : undefined;
- export function compareStrings(a: string | undefined, b: string | undefined, ignoreCase?: boolean) {
+ export function compareStrings(a: string | undefined, b: string | undefined, ignoreCase: boolean) {
if (a === b) return 0;
if (a === undefined) return -1;
if (b === undefined) return +1;
@@ -23,13 +33,8 @@ namespace Collections {
}
export namespace compareStrings {
- export function caseSensitive(a: string | undefined, b: string | undefined) {
- return compareStrings(a, b, /*ignoreCase*/ false);
- }
-
- export function caseInsensitive(a: string | undefined, b: string | undefined) {
- return compareStrings(a, b, /*ignoreCase*/ true);
- }
+ export function caseSensitive(a: string | undefined, b: string | undefined) { return compareStrings(a, b, /*ignoreCase*/ false); }
+ export function caseInsensitive(a: string | undefined, b: string | undefined) { return compareStrings(a, b, /*ignoreCase*/ true); }
}
function insertAt(array: T[], index: number, value: T) {
@@ -50,7 +55,7 @@ namespace Collections {
/**
* A collection of key/value pairs sorted by key.
*/
- export class KeyedCollection {
+ export class SortedCollection {
private _comparer: (a: K, b: K) => number;
private _keys: K[] = [];
private _values: V[] = [];
@@ -145,6 +150,14 @@ namespace Collections {
const undefinedSentinel = {};
+ function escapeKey(text: string) {
+ return (text.length >= 2 && text.charAt(0) === "_" && text.charAt(1) === "_" ? "_" + text : text);
+ }
+
+ function unescapeKey(text: string) {
+ return (text.length >= 3 && text.charAt(0) === "_" && text.charAt(1) === "_" && text.charAt(2) === "_" ? text.slice(1) : text);
+ }
+
/**
* A collection of metadata that supports inheritance.
*/
@@ -173,24 +186,25 @@ namespace Collections {
}
public has(key: string): boolean {
- return this._map[key] !== undefined;
+ return this._map[escapeKey(key)] !== undefined;
}
public get(key: string): any {
- const value = this._map[key];
+ const value = this._map[escapeKey(key)];
return value === undefinedSentinel ? undefined : value;
}
public set(key: string, value: any): this {
- this._map[key] = value === undefined ? undefinedSentinel : value;
+ this._map[escapeKey(key)] = value === undefined ? undefinedSentinel : value;
this._size = -1;
this._version++;
return this;
}
public delete(key: string): boolean {
- if (this._map[key] !== undefined) {
- delete this._map[key];
+ const escapedKey = escapeKey(key);
+ if (this._map[escapedKey] !== undefined) {
+ delete this._map[escapedKey];
this._size = -1;
this._version++;
return true;
@@ -206,7 +220,7 @@ namespace Collections {
public forEach(callback: (value: any, key: string, map: this) => void) {
for (const key in this._map) {
- callback(this._map[key], key, this);
+ callback(this._map[key], unescapeKey(key), this);
}
}
}
diff --git a/src/harness/events.ts b/src/harness/events.ts
new file mode 100644
index 00000000000..9443bad9dfe
--- /dev/null
+++ b/src/harness/events.ts
@@ -0,0 +1,26 @@
+///
+namespace events {
+ const _events = require("events");
+
+ export const EventEmitter: {
+ new (): EventEmitter;
+ prototype: EventEmitter;
+ defaultMaxListeners: number;
+ } = _events.EventEmitter;
+
+ export interface EventEmitter {
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
+ once: this["on"];
+ addListener: this["on"];
+ prependListener: this["on"];
+ prependOnceListener: this["on"];
+ removeListener: this["on"];
+ removeAllListeners(event?: string | symbol): this;
+ setMaxListeners(n: number): this;
+ getMaxListeners(): number;
+ listeners(event: string | symbol): Function[];
+ emit(event: string | symbol, ...args: any[]): boolean;
+ eventNames(): (string | symbol)[];
+ listenerCount(type: string | symbol): number;
+ }
+}
\ No newline at end of file
diff --git a/src/harness/harness.ts b/src/harness/harness.ts
index f24ee950d7d..f9340088279 100644
--- a/src/harness/harness.ts
+++ b/src/harness/harness.ts
@@ -32,7 +32,6 @@
// this will work in the browser via browserify
var _chai: typeof chai = require("chai");
var assert: typeof _chai.assert = _chai.assert;
-declare var __dirname: string; // Node-specific
var global: NodeJS.Global = Function("return this").call(undefined);
declare var window: {};
@@ -43,9 +42,13 @@ interface XMLHttpRequest {
readonly readyState: number;
readonly responseText: string;
readonly status: number;
+ readonly statusText: string;
open(method: string, url: string, async?: boolean, user?: string, password?: string): void;
send(data?: string): void;
setRequestHeader(header: string, value: string): void;
+ getAllResponseHeaders(): string;
+ getResponseHeader(header: string): string | null;
+ overrideMimeType(mime: string): void;
}
/* tslint:enable:no-var-keyword */
@@ -489,16 +492,23 @@ namespace Harness {
fileExists(fileName: string): boolean;
directoryExists(path: string): boolean;
deleteFile(fileName: string): void;
- listFiles(path: string, filter: RegExp, options?: { recursive?: boolean }): string[];
+ listFiles(path: string, filter?: RegExp, options?: { recursive?: boolean }): string[];
log(text: string): void;
- getMemoryUsage?(): number;
args(): string[];
getExecutingFilePath(): string;
exit(exitCode?: number): void;
readDirectory(path: string, extension?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[];
+ getAccessibleFileSystemEntries(dirname: string): FileSystemEntries;
tryEnableSourceMapsForHost?(): void;
getEnvironmentVariable?(name: string): string;
+ getMemoryUsage?(): number;
}
+
+ export interface FileSystemEntries {
+ files: string[];
+ directories: string[];
+ }
+
export let IO: IO;
// harness always uses one kind of new line
@@ -508,253 +518,380 @@ namespace Harness {
// Root for file paths that are stored in a virtual file system
export const virtualFileSystemRoot = "/";
- namespace IOImpl {
- export namespace Node {
- declare const require: any;
- let fs: any, pathModule: any;
- if (require) {
- fs = require("fs");
- pathModule = require("path");
- }
- else {
- fs = pathModule = {};
- }
-
- export const resolvePath = (path: string) => ts.sys.resolvePath(path);
- export const getCurrentDirectory = () => ts.sys.getCurrentDirectory();
- export const newLine = () => harnessNewLine;
- export const useCaseSensitiveFileNames = () => ts.sys.useCaseSensitiveFileNames;
- export const args = () => ts.sys.args;
- export const getExecutingFilePath = () => ts.sys.getExecutingFilePath();
- export const exit = (exitCode: number) => ts.sys.exit(exitCode);
- export const getDirectories: typeof IO.getDirectories = path => ts.sys.getDirectories(path);
-
- export const readFile: typeof IO.readFile = path => ts.sys.readFile(path);
- export const writeFile: typeof IO.writeFile = (path, content) => ts.sys.writeFile(path, content);
- export const fileExists: typeof IO.fileExists = fs.existsSync;
- export const log: typeof IO.log = s => console.log(s);
- export const getEnvironmentVariable: typeof IO.getEnvironmentVariable = name => ts.sys.getEnvironmentVariable(name);
-
- export function tryEnableSourceMapsForHost() {
- if (ts.sys.tryEnableSourceMapsForHost) {
- ts.sys.tryEnableSourceMapsForHost();
- }
- }
- export const readDirectory: typeof IO.readDirectory = (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth);
-
- export function createDirectory(path: string) {
- if (!directoryExists(path)) {
- fs.mkdirSync(path);
- }
- }
-
- export function deleteFile(path: string) {
- try {
- fs.unlinkSync(path);
- }
- catch (e) {
- }
- }
-
- export function directoryExists(path: string): boolean {
- return fs.existsSync(path) && fs.statSync(path).isDirectory();
- }
-
- export function directoryName(path: string) {
- const dirPath = pathModule.dirname(path);
- // Node will just continue to repeat the root path, rather than return null
- return dirPath === path ? undefined : dirPath;
- }
-
- export let listFiles: typeof IO.listFiles = (path, spec?, options?) => {
- options = options || {};
-
- function filesInFolder(folder: string): string[] {
- let paths: string[] = [];
-
- const files = fs.readdirSync(folder);
- for (let i = 0; i < files.length; i++) {
- const pathToFile = pathModule.join(folder, files[i]);
- const stat = fs.statSync(pathToFile);
- if (options.recursive && stat.isDirectory()) {
- paths = paths.concat(filesInFolder(pathToFile));
- }
- else if (stat.isFile() && (!spec || files[i].match(spec))) {
- paths.push(pathToFile);
- }
- }
-
- return paths;
- }
-
- return filesInFolder(path);
- };
-
- export let getMemoryUsage: typeof IO.getMemoryUsage = () => {
- if (global.gc) {
- global.gc();
- }
- return process.memoryUsage().heapUsed;
- };
+ function createNodeIO(): IO {
+ let fs: any, pathModule: any;
+ if (require) {
+ fs = require("fs");
+ pathModule = require("path");
+ }
+ else {
+ fs = pathModule = {};
}
- export namespace Network {
- const serverRoot = "http://localhost:8888/";
-
- export const newLine = () => harnessNewLine;
- export const useCaseSensitiveFileNames = () => false;
- export const getCurrentDirectory = () => "";
- export const args = () => [];
- export const getExecutingFilePath = () => "";
- export const exit = ts.noop;
- export const getDirectories = () => [];
-
- export let log = (s: string) => console.log(s);
-
- namespace Http {
- function waitForXHR(xhr: XMLHttpRequest) {
- while (xhr.readyState !== 4) { }
- return { status: xhr.status, responseText: xhr.responseText };
- }
-
- /// Ask the server to use node's path.resolve to resolve the given path
-
- export interface XHRResponse {
- status: number;
- responseText: string;
- }
-
- /// Ask the server for the contents of the file at the given URL via a simple GET request
- export function getFileFromServerSync(url: string): XHRResponse {
- const xhr = new XMLHttpRequest();
- try {
- xhr.open("GET", url, /*async*/ false);
- xhr.send();
- }
- catch (e) {
- return { status: 404, responseText: undefined };
- }
-
- return waitForXHR(xhr);
- }
-
- /// Submit a POST request to the server to do the given action (ex WRITE, DELETE) on the provided URL
- export function writeToServerSync(url: string, action: string, contents?: string): XHRResponse {
- const xhr = new XMLHttpRequest();
- try {
- const actionMsg = "?action=" + action;
- xhr.open("POST", url + actionMsg, /*async*/ false);
- xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
- xhr.send(contents);
- }
- catch (e) {
- log(`XHR Error: ${e}`);
- return { status: 500, responseText: undefined };
- }
-
- return waitForXHR(xhr);
- }
+ function deleteFile(path: string) {
+ try {
+ fs.unlinkSync(path);
}
-
- export function createDirectory() {
- // Do nothing (?)
- }
-
- export function deleteFile(path: string) {
- Http.writeToServerSync(serverRoot + path, "DELETE");
- }
-
- export function directoryExists(): boolean {
- return false;
- }
-
- function directoryNameImpl(path: string) {
- let dirPath = path;
- // root of the server
- if (dirPath.match(/localhost:\d+$/) || dirPath.match(/localhost:\d+\/$/)) {
- dirPath = undefined;
- // path + fileName
- }
- else if (dirPath.indexOf(".") === -1) {
- dirPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
- // path
- }
- else {
- // strip any trailing slash
- if (dirPath.match(/.*\/$/)) {
- dirPath = dirPath.substring(0, dirPath.length - 2);
- }
- dirPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
- }
-
- return dirPath;
- }
- export let directoryName: typeof IO.directoryName = Utils.memoize(directoryNameImpl, path => path);
-
- export function resolvePath(path: string) {
- const response = Http.getFileFromServerSync(serverRoot + path + "?resolve=true");
- if (response.status === 200) {
- return response.responseText;
- }
- else {
- return undefined;
- }
- }
-
- export function fileExists(path: string): boolean {
- const response = Http.getFileFromServerSync(serverRoot + path);
- return response.status === 200;
- }
-
- export const listFiles = Utils.memoize((path: string, spec?: RegExp, options?: { recursive?: boolean }): string[] => {
- const response = Http.getFileFromServerSync(serverRoot + path);
- if (response.status === 200) {
- let results = response.responseText.split(",");
- if (spec) {
- results = results.filter(file => spec.test(file));
- }
- if (options && !options.recursive) {
- results = results.filter(file => (ts.getDirectoryPath(ts.normalizeSlashes(file)) === path));
- }
- return results;
- }
- else {
- return [""];
- }
- }, (path: string, spec?: RegExp, options?: { recursive?: boolean }) => `${path}|${spec}|${options ? options.recursive : undefined}`);
-
- export function readFile(file: string): string | undefined {
- const response = Http.getFileFromServerSync(serverRoot + file);
- if (response.status === 200) {
- return response.responseText;
- }
- else {
- return undefined;
- }
- }
-
- export function writeFile(path: string, contents: string) {
- Http.writeToServerSync(serverRoot + path, "WRITE", contents);
- }
-
- export function readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[], depth?: number) {
- const fs = new Utils.VirtualFileSystem(path, useCaseSensitiveFileNames());
- for (const file of listFiles(path)) {
- fs.addFile(file);
- }
- return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), getCurrentDirectory(), depth, path => {
- const entry = fs.getEntry(path);
- if (entry instanceof Utils.VirtualDirectory) {
- const directory = entry;
- return {
- files: ts.map(directory.getFiles(), f => f.name),
- directories: ts.map(directory.getDirectories(), d => d.name)
- };
- }
- return { files: [], directories: [] };
- });
+ catch (e) {
}
}
+
+ function directoryName(path: string) {
+ const dirPath = pathModule.dirname(path);
+ // Node will just continue to repeat the root path, rather than return null
+ return dirPath === path ? undefined : dirPath;
+ }
+
+ function listFiles(path: string, spec: RegExp, options?: { recursive?: boolean }) {
+ options = options || {};
+
+ function filesInFolder(folder: string): string[] {
+ let paths: string[] = [];
+
+ const files = fs.readdirSync(folder);
+ for (let i = 0; i < files.length; i++) {
+ const pathToFile = pathModule.join(folder, files[i]);
+ const stat = fs.statSync(pathToFile);
+ if (options.recursive && stat.isDirectory()) {
+ paths = paths.concat(filesInFolder(pathToFile));
+ }
+ else if (stat.isFile() && (!spec || files[i].match(spec))) {
+ paths.push(pathToFile);
+ }
+ }
+
+ return paths;
+ }
+
+ return filesInFolder(path);
+ }
+
+ function getAccessibleFileSystemEntries(dirname: string): FileSystemEntries {
+ try {
+ const entries: string[] = fs.readdirSync(dirname || ".").sort(ts.sys.useCaseSensitiveFileNames ? ts.compareStrings : ts.compareStringsCaseInsensitive);
+ const files: string[] = [];
+ const directories: string[] = [];
+ for (const entry of entries) {
+ if (entry === "." || entry === "..") continue;
+ const name = vpath.combine(dirname, entry);
+ try {
+ const stat = fs.statSync(name);
+ if (!stat) continue;
+ if (stat.isFile()) {
+ files.push(entry);
+ }
+ else if (stat.isDirectory()) {
+ directories.push(entry);
+ }
+ }
+ catch (e) { }
+ }
+ return { files, directories };
+ }
+ catch (e) {
+ return { files: [], directories: [] };
+ }
+ }
+
+ return {
+ newLine: () => harnessNewLine,
+ getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
+ resolvePath: (path: string) => ts.sys.resolvePath(path),
+ readFile: path => ts.sys.readFile(path),
+ writeFile: (path, content) => ts.sys.writeFile(path, content),
+ directoryName,
+ getDirectories: path => ts.sys.getDirectories(path),
+ createDirectory: path => ts.sys.createDirectory(path),
+ fileExists: path => ts.sys.fileExists(path),
+ directoryExists: path => ts.sys.directoryExists(path),
+ deleteFile,
+ listFiles,
+ log: s => console.log(s),
+ args: () => ts.sys.args,
+ getExecutingFilePath: () => ts.sys.getExecutingFilePath(),
+ exit: exitCode => ts.sys.exit(exitCode),
+ readDirectory: (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth),
+ getAccessibleFileSystemEntries,
+ tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost && ts.sys.tryEnableSourceMapsForHost(),
+ getMemoryUsage: () => ts.sys.getMemoryUsage && ts.sys.getMemoryUsage(),
+ getEnvironmentVariable: name => ts.sys.getEnvironmentVariable(name),
+ };
+ }
+
+ interface URL {
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ password: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ username: string;
+ toString(): string;
+ }
+
+ declare var URL: {
+ prototype: URL;
+ new(url: string, base?: string | URL): URL;
+ };
+
+ function createBrowserIO(): IO {
+ const serverRoot = new URL("http://localhost:8888/");
+
+ interface HttpHeaders {
+ [key: string]: string | string[] | undefined;
+ }
+
+ const HttpHeaders = {
+ combine(left: HttpHeaders | undefined, right: HttpHeaders | undefined): HttpHeaders {
+ return left && right ? { ...left, ...right } :
+ left ? { ...left } :
+ right ? { ...right } :
+ {};
+ },
+ writeRequestHeaders(xhr: XMLHttpRequest, headers: HttpHeaders) {
+ for (const key in headers) {
+ if (!headers.hasOwnProperty(key)) continue;
+ const keyLower = key.toLowerCase();
+
+ if (keyLower === "access-control-allow-origin" || keyLower === "content-length") continue;
+ const values = headers[key];
+ const value = Array.isArray(values) ? values.join(",") : values;
+ if (keyLower === "content-type") {
+ xhr.overrideMimeType(value);
+ continue;
+ }
+
+ xhr.setRequestHeader(key, value);
+ }
+ },
+ readResponseHeaders(xhr: XMLHttpRequest): HttpHeaders {
+ const allHeaders = xhr.getAllResponseHeaders();
+ const headers: HttpHeaders = {};
+ for (const header of allHeaders.split(/\r\n/g)) {
+ const colonIndex = header.indexOf(":");
+ if (colonIndex >= 0) {
+ const key = header.slice(0, colonIndex).trim();
+ const value = header.slice(colonIndex + 1).trim();
+ const values = value.split(",");
+ headers[key] = values.length > 1 ? values : value;
+ }
+ }
+ return headers;
+ }
+ };
+
+ interface HttpContent {
+ headers: HttpHeaders;
+ content: string;
+ }
+
+ const HttpContent = {
+ create(headers: HttpHeaders, content: string): HttpContent {
+ return { headers, content };
+ },
+ fromMediaType(mediaType: string, content: string) {
+ return HttpContent.create({ "Content-Type": mediaType }, content);
+ },
+ text(content: string) {
+ return HttpContent.fromMediaType("text/plain", content);
+ },
+ json(content: object) {
+ return HttpContent.fromMediaType("application/json", JSON.stringify(content));
+ },
+ readResponseContent(xhr: XMLHttpRequest) {
+ if (typeof xhr.responseText === "string") {
+ return HttpContent.create({
+ "Content-Type": xhr.getResponseHeader("Content-Type") || undefined,
+ "Content-Length": xhr.getResponseHeader("Content-Length") || undefined
+ }, xhr.responseText);
+ }
+ return undefined;
+ }
+ };
+
+ interface HttpRequestMessage {
+ method: string;
+ url: URL;
+ headers: HttpHeaders;
+ content?: HttpContent;
+ }
+
+ const HttpRequestMessage = {
+ create(method: string, url: string | URL, headers: HttpHeaders = {}, content?: HttpContent): HttpRequestMessage {
+ if (typeof url === "string") url = new URL(url);
+ return { method, url, headers, content };
+ },
+ options(url: string | URL) {
+ return HttpRequestMessage.create("OPTIONS", url);
+ },
+ head(url: string | URL) {
+ return HttpRequestMessage.create("HEAD", url);
+ },
+ get(url: string | URL) {
+ return HttpRequestMessage.create("GET", url);
+ },
+ delete(url: string | URL) {
+ return HttpRequestMessage.create("DELETE", url);
+ },
+ put(url: string | URL, content: HttpContent) {
+ return HttpRequestMessage.create("PUT", url, {}, content);
+ },
+ post(url: string | URL, content: HttpContent) {
+ return HttpRequestMessage.create("POST", url, {}, content);
+ },
+ };
+
+ interface HttpResponseMessage {
+ statusCode: number;
+ statusMessage: string;
+ headers: HttpHeaders;
+ content?: HttpContent;
+ }
+
+ const HttpResponseMessage = {
+ create(statusCode: number, statusMessage: string, headers: HttpHeaders = {}, content?: HttpContent): HttpResponseMessage {
+ return { statusCode, statusMessage, headers, content };
+ },
+ notFound(): HttpResponseMessage {
+ return HttpResponseMessage.create(404, "Not Found");
+ },
+ hasSuccessStatusCode(response: HttpResponseMessage) {
+ return response.statusCode === 304 || (response.statusCode >= 200 && response.statusCode < 300);
+ },
+ readResponseMessage(xhr: XMLHttpRequest) {
+ return HttpResponseMessage.create(
+ xhr.status,
+ xhr.statusText,
+ HttpHeaders.readResponseHeaders(xhr),
+ HttpContent.readResponseContent(xhr));
+ }
+ };
+
+ function send(request: HttpRequestMessage): HttpResponseMessage {
+ const xhr = new XMLHttpRequest();
+ try {
+ HttpHeaders.writeRequestHeaders(xhr, request.headers);
+ HttpHeaders.writeRequestHeaders(xhr, request.content && request.content.headers);
+ xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
+ xhr.open(request.method, request.url.toString(), /*async*/ false);
+ xhr.send(request.content && request.content.content);
+ while (xhr.readyState !== 4); // block until ready
+ return HttpResponseMessage.readResponseMessage(xhr);
+ }
+ catch (e) {
+ return HttpResponseMessage.notFound();
+ }
+ }
+
+ let caseSensitivity: "CI" | "CS" | undefined;
+
+ function useCaseSensitiveFileNames() {
+ if (!caseSensitivity) {
+ const response = send(HttpRequestMessage.options(new URL("*", serverRoot)));
+ const xCaseSensitivity = response.headers["X-Case-Sensitivity"];
+ caseSensitivity = xCaseSensitivity === "CS" ? "CS" : "CI";
+ }
+ return caseSensitivity === "CS";
+ }
+
+ function resolvePath(path: string) {
+ const response = send(HttpRequestMessage.post(new URL("/api/resolve", serverRoot), HttpContent.text(path)));
+ return HttpResponseMessage.hasSuccessStatusCode(response) && response.content ? response.content.content : undefined;
+ }
+
+ function readFile(path: string): string | undefined {
+ const response = send(HttpRequestMessage.get(new URL(path, serverRoot)));
+ return HttpResponseMessage.hasSuccessStatusCode(response) && response.content ? response.content.content : undefined;
+ }
+
+ function writeFile(path: string, contents: string) {
+ send(HttpRequestMessage.put(new URL(path, serverRoot), HttpContent.text(contents)));
+ }
+
+ function fileExists(path: string): boolean {
+ const response = send(HttpRequestMessage.head(new URL(path, serverRoot)));
+ return HttpResponseMessage.hasSuccessStatusCode(response);
+ }
+
+ function directoryExists(path: string): boolean {
+ const response = send(HttpRequestMessage.post(new URL("/api/directoryExists", serverRoot), HttpContent.text(path)));
+ return HttpResponseMessage.hasSuccessStatusCode(response)
+ && (response.content && response.content.content) === "true";
+ }
+
+ function deleteFile(path: string) {
+ send(HttpRequestMessage.delete(new URL(path, serverRoot)));
+ }
+
+ function directoryName(path: string) {
+ const url = new URL(path, serverRoot);
+ return ts.getDirectoryPath(ts.normalizeSlashes(url.pathname || "/"));
+ }
+
+ function listFiles(dirname: string, spec?: RegExp, options?: { recursive?: boolean }): string[] {
+ if (spec || (options && !options.recursive)) {
+ let results = IO.listFiles(dirname);
+ if (spec) {
+ results = results.filter(file => spec.test(file));
+ }
+ if (options && !options.recursive) {
+ results = results.filter(file => ts.getDirectoryPath(ts.normalizeSlashes(file)) === dirname);
+ }
+ return results;
+ }
+
+ const response = send(HttpRequestMessage.post(new URL("/api/listFiles", serverRoot), HttpContent.text(dirname)));
+ return HttpResponseMessage.hasSuccessStatusCode(response)
+ && response.content
+ && response.content.headers["Content-Type"] === "application/json"
+ ? JSON.parse(response.content.content)
+ : [];
+ }
+
+ function readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[], depth?: number) {
+ const fs = new vfs.VirtualFileSystem(path, useCaseSensitiveFileNames());
+ fs.addFiles(IO.listFiles(path));
+ return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), /*currentDirectory*/ "", depth, path => getAccessibleVirtualFileSystemEntries(fs, path));
+ }
+
+ function getAccessibleFileSystemEntries(dirname: string): FileSystemEntries {
+ const fs = new vfs.VirtualFileSystem(dirname, useCaseSensitiveFileNames());
+ fs.addFiles(IO.listFiles(dirname));
+ return getAccessibleVirtualFileSystemEntries(fs, dirname);
+ }
+
+ function getAccessibleVirtualFileSystemEntries(fs: vfs.VirtualFileSystem, dirname: string): FileSystemEntries {
+ const directory = fs.getDirectory(dirname);
+ return directory
+ ? { files: directory.getFileNames(), directories: directory.getDirectoryNames() }
+ : { files: [], directories: [] };
+ }
+
+ return {
+ newLine: () => harnessNewLine,
+ getCurrentDirectory: () => "",
+ useCaseSensitiveFileNames,
+ resolvePath,
+ readFile,
+ writeFile,
+ directoryName: Utils.memoize(directoryName, path => path),
+ getDirectories: () => [],
+ createDirectory: () => {},
+ fileExists,
+ directoryExists,
+ deleteFile,
+ listFiles: Utils.memoize(listFiles, (path, spec, options) => `${path}|${spec}|${options ? options.recursive === true : true}`),
+ log: s => console.log(s),
+ args: () => [],
+ getExecutingFilePath: () => "",
+ exit: () => {},
+ readDirectory,
+ getAccessibleFileSystemEntries
+ };
}
export function mockHash(s: string): string {
@@ -764,10 +901,10 @@ namespace Harness {
const environment = Utils.getExecutionEnvironment();
switch (environment) {
case Utils.ExecutionEnvironment.Node:
- IO = IOImpl.Node;
+ IO = createNodeIO();
break;
case Utils.ExecutionEnvironment.Browser:
- IO = IOImpl.Network;
+ IO = createBrowserIO();
break;
default:
throw new Error(`Unknown value '${environment}' for ExecutionEnvironment.`);
diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts
index 9e353a14351..ebfd9b9b02b 100644
--- a/src/harness/harnessLanguageService.ts
+++ b/src/harness/harnessLanguageService.ts
@@ -123,7 +123,7 @@ namespace Harness.LanguageService {
}
export class LanguageServiceAdapterHost {
- protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/ false);
+ protected virtualFileSystem: vfs.VirtualFileSystem = new vfs.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/ false);
constructor(protected cancellationToken = DefaultHostCancellationToken.Instance,
protected settings = ts.getDefaultCompilerOptions()) {
@@ -135,8 +135,8 @@ namespace Harness.LanguageService {
public getFilenames(): string[] {
const fileNames: string[] = [];
- for (const virtualEntry of this.virtualFileSystem.getFiles({ recursive: true })) {
- const scriptInfo = virtualEntry.metadata.get("scriptInfo");
+ for (const virtualEntry of this.virtualFileSystem.getDirectory("/").getFiles({ recursive: true })) {
+ const scriptInfo = virtualEntry.metadata.get("scriptInfo") as ScriptInfo;
if (scriptInfo && scriptInfo.isRootFile) {
// only include root files here
// usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir.
@@ -160,7 +160,7 @@ namespace Harness.LanguageService {
const script = file && file.metadata.get("scriptInfo") as ScriptInfo;
if (script) {
script.editContent(start, end, newText);
- file.setContent(script.content);
+ file.content = script.content;
return;
}
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index 2838ba5b0c4..dd8719a1bee 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -99,6 +99,7 @@
"runner.ts",
"collections.ts",
"vpath.ts",
+ "events.ts",
"vfs.ts",
"virtualFileSystemWithWatch.ts",
"../server/protocol.ts",
diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts
index bd33e632d6f..42ac54b076f 100644
--- a/src/harness/vfs.ts
+++ b/src/harness/vfs.ts
@@ -1,70 +1,533 @@
///
///
///
+///
///
-namespace Utils {
- import compareStrings = Collections.compareStrings;
- import removeAt = ts.orderedRemoveItemAt;
- import KeyedCollection = Collections.KeyedCollection;
- import Metadata = Collections.Metadata;
+namespace vfs {
+ import compareStrings = collections.compareStrings;
+ import KeyedCollection = collections.SortedCollection;
+ import Metadata = collections.Metadata;
+ import EventEmitter = events.EventEmitter;
+ import IO = Harness.IO;
- // import IO = Harness.IO;
+ export interface PathMappings {
+ [path: string]: string;
+ }
- // export interface PathMappings {
- // [path: string]: string;
- // }
+ export interface FileSystemResolver {
+ getEntries(dir: VirtualDirectory): { files: string[], directories: string[] };
+ getContent(file: VirtualFile): string | undefined;
+ }
- // export interface FileSystemResolver {
- // getEntries(dir: VirtualDirectory): { files: string[], directories: string[] };
- // getContent(file: VirtualFile): string | undefined;
- // }
+ export type ContentResolver = FileSystemResolver["getContent"];
- // function createMapper(ignoreCase: boolean, map: PathMappings | undefined) {
- // if (!map) return identity;
- // const roots = Object.keys(map);
- // const patterns = roots.map(root => createPattern(root, ignoreCase));
- // return function (path: string) {
- // for (let i = 0; i < patterns.length; i++) {
- // const match = patterns[i].exec(path);
- // if (match) {
- // const prefix = path.slice(0, match.index);
- // const suffix = path.slice(match.index + match[0].length);
- // return vpath.combine(prefix, map[roots[i]], suffix);
- // }
- // }
- // return path;
- // };
- // }
+ function identityMapper(path: string) { return path; }
- // function createPattern(path: string, ignoreCase: boolean) {
- // path = vpath.normalizeSlashes(path);
- // const components = vpath.parse(path);
- // let pattern = "";
- // for (let i = 1; i < components.length; i++) {
- // const component = components[i];
- // if (pattern) pattern += "/";
- // pattern += escapeRegExp(component);
- // }
- // pattern = (components[0] ? "^" + escapeRegExp(components[0]) : "/") + pattern + "(/|$)";
- // return new RegExp(pattern, ignoreCase ? "i" : "");
- // }
+ function createMapper(ignoreCase: boolean, map: PathMappings | undefined) {
+ if (!map) return identityMapper;
+ const roots = Object.keys(map);
+ const patterns = roots.map(root => createPattern(root, ignoreCase));
+ return function (path: string) {
+ for (let i = 0; i < patterns.length; i++) {
+ const match = patterns[i].exec(path);
+ if (match) {
+ const prefix = path.slice(0, match.index);
+ const suffix = path.slice(match.index + match[0].length);
+ return vpath.combine(prefix, map[roots[i]], suffix);
+ }
+ }
+ return path;
+ };
+ }
- // export function createResolver(io: IO, map?: PathMappings): FileSystemResolver {
- // const mapper = createMapper(!io.useCaseSensitiveFileNames(), map);
- // return {
- // getEntries(dir) {
- // return io.getAccessibleFileSystemEntries(mapper(dir.path));
- // },
- // getContent(file) {
- // return io.readFile(mapper(file.path));
- // }
- // };
- // }
+ const reservedCharacterRegExp = /[^\w\s\/]/g;
- export type FindAxis = "ancestors" | "ancestors-or-self" | "self" | "descendents-or-self" | "descendents";
+ function escapeRegExp(pattern: string) {
+ return pattern.replace(reservedCharacterRegExp, match => "\\" + match);
+ }
- export abstract class VirtualFileSystemEntry {
+ function createPattern(path: string, ignoreCase: boolean) {
+ path = vpath.normalizeSeparators(path);
+ const components = vpath.parse(path);
+ let pattern = "";
+ for (let i = 1; i < components.length; i++) {
+ const component = components[i];
+ if (pattern) pattern += "/";
+ pattern += escapeRegExp(component);
+ }
+ pattern = (components[0] ? "^" + escapeRegExp(components[0]) : "/") + pattern + "(/|$)";
+ return new RegExp(pattern, ignoreCase ? "i" : "");
+ }
+
+ export function createResolver(io: IO, map?: PathMappings): FileSystemResolver {
+ const mapper = createMapper(!io.useCaseSensitiveFileNames(), map);
+ return {
+ getEntries(dir) {
+ return io.getAccessibleFileSystemEntries(mapper(dir.path));
+ },
+ getContent(file) {
+ return io.readFile(mapper(file.path));
+ }
+ };
+ }
+
+ export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendents-or-self" | "descendents";
+ export type FileSystemChange = "added" | "modified" | "removed";
+ export type VirtualEntry = VirtualFile | VirtualDirectory;
+ export type VirtualSymlink = VirtualFileSymlink | VirtualDirectorySymlink;
+
+ type VirtualEntryView = VirtualFileView | VirtualDirectoryView;
+
+ interface FileWatcherEntry {
+ watcher: (path: string, change: FileSystemChange) => void;
+ }
+
+ interface DirectoryWatcherEntryArray extends Array {
+ recursiveCount?: number;
+ }
+
+ interface DirectoryWatcherEntry {
+ watcher: (path: string) => void;
+ recursive: boolean;
+ }
+
+ export abstract class VirtualFileSystemObject extends EventEmitter {
private _readonly = false;
+
+ /**
+ * Gets a value indicating whether this entry is read-only.
+ */
+ public get isReadOnly(): boolean {
+ return this._readonly;
+ }
+
+ public makeReadOnly(): void {
+ this.makeReadOnlyCore();
+ this._readonly = true;
+ }
+
+ protected abstract makeReadOnlyCore(): void;
+
+ protected writePreamble(): void {
+ if (this._readonly) throw new Error("Cannot modify a frozen entry.");
+ }
+ }
+
+ export class VirtualFileSystem extends VirtualFileSystemObject {
+ private static _builtLocal: VirtualFileSystem | undefined;
+ private static _builtLocalCI: VirtualFileSystem | undefined;
+ private static _builtLocalCS: VirtualFileSystem | undefined;
+
+ private _root: VirtualRoot;
+ private _useCaseSensitiveFileNames: boolean;
+ private _currentDirectory: string;
+ private _currentDirectoryStack: string[] | undefined;
+ private _shadowRoot: VirtualFileSystem | undefined;
+ private _watchedFiles: KeyedCollection | undefined;
+ private _watchedDirectories: KeyedCollection | undefined;
+ private _onRootFileSystemChange: (path: string, change: FileSystemChange) => void;
+
+ constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) {
+ super();
+ this._currentDirectory = currentDirectory.replace(/\\/g, "/");
+ this._useCaseSensitiveFileNames = useCaseSensitiveFileNames;
+ this._onRootFileSystemChange = (path, change) => this.onRootFileSystemChange(path, change);
+ }
+
+ /**
+ * Gets the file system shadowed by this instance.
+ */
+ public get shadowRoot(): VirtualFileSystem | undefined {
+ return this._shadowRoot;
+ }
+
+ /**
+ * Gets a value indicating whether to use case sensitive file names.
+ */
+ public get useCaseSensitiveFileNames() {
+ return this._useCaseSensitiveFileNames;
+ }
+
+ /**
+ * Gets the path to the current directory.
+ */
+ public get currentDirectory() {
+ return this._currentDirectory;
+ }
+
+ private get root(): VirtualRoot {
+ if (this._root === undefined) {
+ if (this._shadowRoot) {
+ this._root = this._shadowRoot.root._shadow(this);
+ }
+ else {
+ this._root = new VirtualRoot(this);
+ }
+ this._root.addListener("fileSystemChange", this._onRootFileSystemChange);
+ if (this.isReadOnly) this._root.makeReadOnly();
+ }
+ return this._root;
+ }
+
+ /**
+ * Gets a virtual file system with the following entries:
+ *
+ * | path | physical/virtual |
+ * |:-------|:----------------------|
+ * | /.ts | physical: built/local |
+ * | /.lib | physical: tests/lib |
+ * | /.test | virtual |
+ */
+ public static getBuiltLocal(useCaseSensitiveFileNames: boolean = IO.useCaseSensitiveFileNames()): VirtualFileSystem {
+ let vfs = useCaseSensitiveFileNames ? this._builtLocalCS : this._builtLocalCI;
+ if (!vfs) {
+ vfs = this._builtLocal;
+ if (!vfs) {
+ const resolver = createResolver(IO, {
+ "/.ts": __dirname,
+ "/.lib": vpath.resolve(__dirname, "../../tests/lib")
+ });
+ vfs = new VirtualFileSystem("/", IO.useCaseSensitiveFileNames());
+ vfs.addDirectory("/.ts", resolver);
+ vfs.addDirectory("/.lib", resolver);
+ vfs.addDirectory("/.test");
+ vfs.changeDirectory("/.test");
+ vfs.makeReadOnly();
+ this._builtLocal = vfs;
+ }
+ if (vfs._useCaseSensitiveFileNames !== useCaseSensitiveFileNames) {
+ vfs = vfs.shadow();
+ vfs._useCaseSensitiveFileNames = useCaseSensitiveFileNames;
+ vfs.makeReadOnly();
+ }
+ return useCaseSensitiveFileNames
+ ? this._builtLocalCS = vfs
+ : this._builtLocalCI = vfs;
+ }
+ return vfs;
+ }
+
+ /**
+ * Gets a value indicating whether to file names are equivalent for the file system's case sensitivity.
+ */
+ public sameName(a: string, b: string) {
+ return compareStrings(a, b, !this.useCaseSensitiveFileNames) === 0;
+ }
+
+ /**
+ * Changes the current directory to the supplied path.
+ */
+ public changeDirectory(path: string) {
+ this.writePreamble();
+ if (path) {
+ this._currentDirectory = vpath.resolve(this._currentDirectory, path);
+ }
+ }
+
+ /**
+ * Pushes the current directory onto the location stack and changes the current directory to the supplied path.
+ */
+ public pushDirectory(path = this.currentDirectory) {
+ this.writePreamble();
+ if (this._currentDirectoryStack === undefined) {
+ this._currentDirectoryStack = [this.currentDirectory];
+ }
+ else {
+ this._currentDirectoryStack.push(this.currentDirectory);
+ }
+ this.changeDirectory(path);
+ }
+
+ /**
+ * Pops the previous directory from the location stack and changes the current directory to that directory.
+ */
+ public popDirectory() {
+ this.writePreamble();
+ const previousDirectory = this._currentDirectoryStack && this._currentDirectoryStack.pop();
+ if (previousDirectory !== undefined) {
+ this._currentDirectory = previousDirectory;
+ }
+ }
+
+ /**
+ * Adds a directory (and all intermediate directories) to a path relative to the current directory.
+ */
+ public addDirectory(path: string, resolver?: FileSystemResolver) {
+ return this.root.addDirectory(vpath.resolve(this.currentDirectory, path), resolver);
+ }
+
+ /**
+ * Adds a file (and all intermediate directories) to a path relative to the current directory.
+ */
+ public addFile(path: string, content?: FileSystemResolver | ContentResolver | string, options?: { overwrite?: boolean }) {
+ return this.root.addFile(vpath.resolve(this.currentDirectory, path), content, options);
+ }
+
+ /**
+ * Adds multiple files (and all intermediate directories) to paths relative to the current directory.
+ */
+ public addFiles(files: string[]) {
+ for (const file of files) {
+ this.addFile(file);
+ }
+ }
+
+ /**
+ * Adds a symbolic link to a target entry for a path relative to the current directory.
+ */
+ public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined;
+ /**
+ * Adds a symbolic link to a target entry for a path relative to the current directory.
+ */
+ public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined;
+ /**
+ * Adds a symbolic link to a target entry for a path relative to the current directory.
+ */
+ public addSymlink(path: string, target: string | VirtualEntry): VirtualSymlink | undefined;
+ public addSymlink(path: string, target: string | VirtualEntry) {
+ if (typeof target === "string") target = vpath.resolve(this.currentDirectory, target);
+ return this.root.addSymlink(vpath.resolve(this.currentDirectory, path), target);
+ }
+
+ /**
+ * Removes a directory (and all of its contents) at a path relative to the current directory.
+ */
+ public removeDirectory(path: string): boolean {
+ return this.root.removeDirectory(vpath.resolve(this.currentDirectory, path));
+ }
+
+ /**
+ * Removes a file at a path relative to the current directory.
+ */
+ public removeFile(path: string): boolean {
+ return this.root.removeFile(vpath.resolve(this.currentDirectory, path));
+ }
+
+ /**
+ * Reads the contents of a file at a path relative to the current directory.
+ */
+ public readFile(path: string): string | undefined {
+ const file = this.getFile(vpath.resolve(this.currentDirectory, path));
+ return file && file.content;
+ }
+
+ /**
+ * Writes the contents of a file at a path relative to the current directory.
+ */
+ public writeFile(path: string, content: string): void {
+ path = vpath.resolve(this.currentDirectory, path);
+ const file = this.getFile(path) || this.addFile(path);
+ if (file) {
+ file.content = content;
+ }
+ }
+
+ /**
+ * Gets a value indicating whether a path relative to the current directory exists and is a directory.
+ */
+ public directoryExists(path: string) {
+ return this.getEntry(path) instanceof VirtualDirectory;
+ }
+
+ /**
+ * Gets a value indicating whether a path relative to the current directory exists and is a file.
+ */
+ public fileExists(path: string) {
+ return this.getEntry(path) instanceof VirtualFile;
+ }
+
+ /**
+ * If an entry is a symbolic link, gets the resolved target of the link. Otherwise, returns the entry.
+ */
+ public getRealEntry(entry: VirtualDirectory): VirtualDirectory | undefined;
+ /**
+ * If an entry is a symbolic link, gets the resolved target of the link. Otherwise, returns the entry.
+ */
+ public getRealEntry(entry: VirtualFile): VirtualFile | undefined;
+ /**
+ * If an entry is a symbolic link, gets the resolved target of the link. Otherwise, returns the entry.
+ */
+ public getRealEntry(entry: VirtualEntry): VirtualEntry | undefined;
+ public getRealEntry(entry: VirtualEntry): VirtualEntry | undefined {
+ if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) {
+ return findTarget(this, entry.targetPath);
+ }
+ return entry;
+ }
+
+ /**
+ * Gets an entry from a path relative to the current directory.
+ */
+ public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
+ /**
+ * Gets an entry from a path relative to the current directory.
+ */
+ public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
+ /**
+ * Gets an entry from a path relative to the current directory.
+ */
+ public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry | undefined;
+ public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) {
+ return this.root.getEntry(vpath.resolve(this.currentDirectory, path), options);
+ }
+
+ /**
+ * Gets a file from a path relative to the current directory.
+ */
+ public getFile(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualFile | undefined {
+ return this.root.getFile(vpath.resolve(this.currentDirectory, path), options);
+ }
+
+ /**
+ * Gets a directory from a path relative to the current directory.
+ */
+ public getDirectory(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualDirectory | undefined {
+ return this.root.getDirectory(vpath.resolve(this.currentDirectory, path), options);
+ }
+
+ /**
+ * Gets the accessible file system entries from a path relative to the current directory.
+ */
+ public getAccessibleFileSystemEntries(path: string) {
+ const entry = this.getEntry(path);
+ if (entry instanceof VirtualDirectory) {
+ return {
+ files: entry.getFiles().map(f => f.name),
+ directories: entry.getDirectories().map(d => d.name)
+ };
+ }
+ return { files: [], directories: [] };
+ }
+
+ /**
+ * Watch a path for changes to a file.
+ */
+ public watchFile(path: string, watcher: (path: string, change: FileSystemChange) => void): ts.FileWatcher {
+ if (!this._watchedFiles) {
+ const pathComparer = this.useCaseSensitiveFileNames ? vpath.compare.caseSensitive : vpath.compare.caseInsensitive;
+ this._watchedFiles = new KeyedCollection(pathComparer);
+ }
+
+ path = vpath.resolve(this.currentDirectory, path);
+ let watchers = this._watchedFiles.get(path);
+ if (!watchers) this._watchedFiles.set(path, watchers = []);
+
+ const entry: FileWatcherEntry = { watcher };
+ watchers.push(entry);
+
+ return {
+ close: () => {
+ const watchers = this._watchedFiles.get(path);
+ if (watchers) {
+ ts.orderedRemoveItem(watchers, entry);
+ if (watchers.length === 0) {
+ this._watchedFiles.delete(path);
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Watch a directory for changes to the contents of the directory.
+ */
+ public watchDirectory(path: string, watcher: (path: string) => void, recursive?: boolean) {
+ if (!this._watchedDirectories) {
+ const pathComparer = this.useCaseSensitiveFileNames ? vpath.compare.caseSensitive : vpath.compare.caseInsensitive;
+ this._watchedDirectories = new KeyedCollection(pathComparer);
+ }
+
+ path = vpath.resolve(this.currentDirectory, path);
+ let watchers = this._watchedDirectories.get(path);
+ if (!watchers) {
+ watchers = [];
+ watchers.recursiveCount = 0;
+ this._watchedDirectories.set(path, watchers);
+ }
+
+ const entry: DirectoryWatcherEntry = { watcher, recursive };
+ watchers.push(entry);
+ if (recursive) watchers.recursiveCount++;
+
+ return {
+ close: () => {
+ const watchers = this._watchedDirectories.get(path);
+ if (watchers) {
+ ts.orderedRemoveItem(watchers, entry);
+ if (watchers.length === 0) {
+ this._watchedDirectories.delete(path);
+ }
+ else if (entry.recursive) {
+ watchers.recursiveCount--;
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Creates a shadow copy of this file system. Changes made to the shadow do not affect
+ * this file system.
+ */
+ public shadow(): VirtualFileSystem {
+ const fs = new VirtualFileSystem(this.currentDirectory, this.useCaseSensitiveFileNames);
+ fs._shadowRoot = this;
+ return fs;
+ }
+
+ public debugPrint(): void {
+ console.log(`cwd: ${this.currentDirectory}`);
+ for (const entry of this.root.getEntries({ recursive: true })) {
+ if (entry instanceof VirtualDirectory) {
+ console.log(entry.path.endsWith("/") ? entry.path : entry.path + "/");
+ if (entry instanceof VirtualDirectorySymlink) {
+ console.log(`-> ${entry.targetPath.endsWith("/") ? entry.targetPath : entry.targetPath + "/"}`);
+ }
+ }
+ else {
+ console.log(entry.path);
+ if (entry instanceof VirtualFileSymlink) {
+ console.log(`-> ${entry.targetPath}`);
+ }
+ }
+ }
+ }
+
+ protected makeReadOnlyCore() {
+ this.root.makeReadOnly();
+ }
+
+ private onRootFileSystemChange(path: string, change: FileSystemChange) {
+ const fileWatchers = this._watchedFiles && this._watchedFiles.get(path);
+ if (fileWatchers) {
+ for (const { watcher } of fileWatchers) {
+ watcher(path, change);
+ }
+ }
+
+ if (this._watchedDirectories && (change === "added" || change === "removed")) {
+ const ignoreCase = !this.useCaseSensitiveFileNames;
+ const dirname = vpath.dirname(path);
+ this._watchedDirectories.forEach((watchers, path) => {
+ const exactMatch = vpath.equals(dirname, path, ignoreCase);
+ if (exactMatch || (watchers.recursiveCount > 0 && vpath.beneath(dirname, path, ignoreCase))) {
+ for (const { recursive, watcher } of watchers) {
+ if (exactMatch || !recursive) {
+ watcher(path);
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ export interface VirtualFileSystemEntry {
+ on(event: "fileSystemChange", listener: (path: string, change: FileSystemChange) => void): this;
+ emit(event: "fileSystemChange", path: string, change: FileSystemChange): boolean;
+ }
+
+ export abstract class VirtualFileSystemEntry extends VirtualFileSystemObject {
private _path: string;
private _metadata: Metadata;
@@ -74,6 +537,7 @@ namespace Utils {
public readonly name: string;
constructor(name: string) {
+ super();
this.name = name;
}
@@ -81,18 +545,19 @@ namespace Utils {
* Gets the file system to which this entry belongs.
*/
public get fileSystem(): VirtualFileSystem {
+ if (!this.parent) throw new TypeError();
return this.parent.fileSystem;
}
/**
- * Gets the container for this entry.
+ * Gets the parent directory for this entry.
*/
- public abstract get parent(): VirtualFileSystemContainer;
+ public abstract get parent(): VirtualDirectory | undefined;
/**
* Gets the entry that this entry shadows.
*/
- public abstract get shadowRoot(): VirtualFileSystemEntry | undefined;
+ public abstract get shadowRoot(): VirtualEntry | undefined;
/**
* Gets metadata about this entry.
@@ -102,31 +567,31 @@ namespace Utils {
}
/**
- * Gets a value indicating whether this entry is read-only.
+ * Gets the full path to this entry.
*/
- public get isReadOnly(): boolean {
- return this._readonly;
- }
-
public get path(): string {
return this._path || (this._path = vpath.combine(this.parent.path, this.name));
}
+ /**
+ * Gets the path to this entry relative to the current directory.
+ */
public get relative(): string {
return this.relativeTo(this.fileSystem.currentDirectory);
}
+ /**
+ * Gets a value indicating whether this entry exists.
+ */
public get exists(): boolean {
return this.parent.exists
- && this.parent.getEntry(this.name) as VirtualFileSystemEntry === this;
+ && this.parent.getEntry(this.name) === this as VirtualFileSystemEntry;
}
- public makeReadOnly(): void {
- this.makeReadOnlyCore();
- this._readonly = true;
- }
-
- public relativeTo(other: string | VirtualFileSystemEntry) {
+ /**
+ * Gets a relative path from this entry to another entry.
+ */
+ public relativeTo(other: string | VirtualEntry) {
if (other) {
const otherPath = typeof other === "string" ? other : other.path;
const ignoreCase = !this.fileSystem.useCaseSensitiveFileNames;
@@ -139,67 +604,80 @@ namespace Utils {
* Creates a file system entry that shadows this file system entry.
* @param parent The container for the shadowed entry.
*/
- public abstract shadow(parent: VirtualFileSystemContainer): VirtualFileSystemEntry;
+ public abstract shadow(parent: VirtualDirectory): VirtualEntry;
- protected abstract makeReadOnlyCore(): void;
-
- protected writePreamble(): void {
- if (this._readonly) throw new Error("Cannot modify a frozen entry.");
+ protected shadowPreamble(parent: VirtualDirectory): void {
+ this.checkShadowParent(parent);
+ this.checkShadowFileSystem(parent.fileSystem);
}
- protected shadowPreamble(parent: VirtualFileSystemContainer): void {
- if (this.parent !== parent.shadowRoot) throw new Error("Incorrect shadow parent");
+ protected checkShadowParent(shadowParent: VirtualDirectory) {
+ if (this.parent !== shadowParent.shadowRoot) throw new Error("Incorrect shadow parent");
+ }
+
+ protected checkShadowFileSystem(shadowFileSystem: VirtualFileSystem) {
let fileSystem: VirtualFileSystem | undefined = this.fileSystem;
while (fileSystem) {
- if (parent.fileSystem === fileSystem) throw new Error("Cannot create shadow for parent in the same file system.");
+ if (shadowFileSystem === fileSystem) throw new Error("Cannot create shadow for parent in the same file system.");
fileSystem = fileSystem.shadowRoot;
}
}
}
- export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry {
- private _childAddedCallbacks: ((entry: VirtualFile | VirtualDirectory) => void)[];
- private _childRemovedCallbacks: ((entry: VirtualFile | VirtualDirectory) => void)[];
+ export interface VirtualDirectory {
+ on(event: "fileSystemChange", listener: (path: string, change: FileSystemChange) => void): this;
+ on(event: "childAdded", listener: (child: VirtualEntry) => void): this;
+ on(event: "childRemoved", listener: (child: VirtualEntry) => void): this;
+ emit(event: "fileSystemChange", path: string, change: FileSystemChange): boolean;
+ emit(event: "childAdded", child: VirtualEntry): boolean;
+ emit(event: "childRemoved", child: VirtualEntry): boolean;
+ }
- public abstract get shadowRoot(): VirtualFileSystemContainer | undefined;
+ export class VirtualDirectory extends VirtualFileSystemEntry {
+ protected _shadowRoot: VirtualDirectory | undefined;
+ private _parent: VirtualDirectory;
+ private _entries: KeyedCollection | undefined;
+ private _resolver: FileSystemResolver | undefined;
+ private _onChildFileSystemChange: (path: string, change: FileSystemChange) => void;
- public addOnChildAdded(callback: (entry: VirtualFile | VirtualDirectory) => void) {
- if (!this._childAddedCallbacks) {
- this._childAddedCallbacks = [callback];
- }
- else if (this._childAddedCallbacks.indexOf(callback) === -1) {
- this._childAddedCallbacks.push(callback);
- }
+ constructor(parent: VirtualDirectory | undefined, name: string, resolver?: FileSystemResolver) {
+ super(name);
+ if (parent === undefined && !(this instanceof VirtualRoot)) throw new TypeError();
+ this._parent = parent;
+ this._entries = undefined;
+ this._resolver = resolver;
+ this._shadowRoot = undefined;
+ this._onChildFileSystemChange = (path, change) => this.onChildFileSystemChange(path, change);
}
- public removeOnChildAdded(callback: (entry: VirtualFile | VirtualDirectory) => void) {
- if (this._childAddedCallbacks) {
- const index = this._childAddedCallbacks.indexOf(callback);
- if (index >= 0) removeAt(this._childAddedCallbacks, index);
- }
+ /**
+ * Gets the container for this entry.
+ */
+ public get parent(): VirtualDirectory | undefined {
+ return this._parent;
}
- public addOnChildRemoved(callback: (entry: VirtualFile | VirtualDirectory) => void) {
- if (!this._childRemovedCallbacks) {
- this._childRemovedCallbacks = [callback];
- }
- else if (this._childRemovedCallbacks.indexOf(callback) === -1) {
- this._childRemovedCallbacks.push(callback);
- }
- }
-
- public removeOnChildRemoved(callback: (entry: VirtualFile | VirtualDirectory) => void) {
- if (this._childRemovedCallbacks) {
- const index = this._childRemovedCallbacks.indexOf(callback);
- if (index >= 0) removeAt(this._childRemovedCallbacks, index);
- }
+ /**
+ * Gets the entry that this entry shadows.
+ */
+ public get shadowRoot(): VirtualDirectory | undefined {
+ return this._shadowRoot;
}
+ /**
+ * Gets the child entries in this directory for the provided options.
+ */
public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile[];
+ /**
+ * Gets the child entries in this directory for the provided options.
+ */
public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory[];
- public getEntries(options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): (VirtualFile | VirtualDirectory)[];
- public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): (VirtualFile | VirtualDirectory)[] {
- const results: (VirtualFile | VirtualDirectory)[] = [];
+ /**
+ * Gets the child entries in this directory for the provided options.
+ */
+ public getEntries(options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry[];
+ public getEntries(options: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualEntry[] {
+ const results: VirtualEntry[] = [];
if (options.recursive) {
this.getOwnEntries().forEach(entry => {
if (entry instanceof VirtualFile) {
@@ -227,14 +705,23 @@ namespace Utils {
return results;
}
+ /**
+ * Gets the child directories in this directory for the provided options.
+ */
public getDirectories(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualDirectory[] {
return this.getEntries({ ...options, kind: "directory" });
}
+ /**
+ * Gets the child files in this directory for the provided options.
+ */
public getFiles(options: { recursive?: boolean, pattern?: RegExp } = {}): VirtualFile[] {
return this.getEntries({ ...options, kind: "file" });
}
+ /**
+ * Gets the names of the child entries in this directory for the provided options.
+ */
public getEntryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): string[] {
return this.getEntries(options).map(entry =>
options && options.qualified ? entry.path :
@@ -242,30 +729,65 @@ namespace Utils {
entry.name);
}
+ /**
+ * Gets the names of the child directories in this directory for the provided options.
+ */
public getDirectoryNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] {
return this.getEntryNames({ ...options, kind: "directory" });
}
+ /**
+ * Gets the names of the child files in this directory for the provided options.
+ */
public getFileNames(options: { recursive?: boolean, qualified?: boolean, pattern?: RegExp } = {}): string[] {
return this.getEntryNames({ ...options, kind: "file" });
}
- public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
- public abstract getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
- public abstract getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined;
+ /**
+ * Gets an entry from a path relative to this directory.
+ */
+ public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
+ /**
+ * Gets an entry from a path relative to this directory.
+ */
+ public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
+ /**
+ * Gets an entry from a path relative to this directory.
+ */
+ public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry | undefined;
+ public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualEntry | undefined {
+ const components = this.parsePath(path);
+ const directory = this.walkContainers(components, /*create*/ false);
+ return directory && directory.getOwnEntry(components[components.length - 1], options);
+ }
+ /**
+ * Gets a directory from a path relative to this directory.
+ */
public getDirectory(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined {
return this.getEntry(path, { ...options, kind: "directory" });
}
+ /**
+ * Gets a file from a path relative to this directory.
+ */
public getFile(path: string, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined {
return this.getEntry(path, { ...options, kind: "file" });
}
- public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
- public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
- public findEntry(path: string, axis: FindAxis, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined;
- public findEntry(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined {
+ /**
+ * Finds an entry for a relative path along the provided axis.
+ */
+ public findEntry(path: string, axis: Axis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
+ /**
+ * Finds an entry for a relative path along the provided axis.
+ */
+ public findEntry(path: string, axis: Axis, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
+ /**
+ * Finds an entry for a relative path along the provided axis.
+ */
+ public findEntry(path: string, axis: Axis, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry | undefined;
+ public findEntry(path: string, axis: Axis, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualEntry | undefined {
const walkAncestors = axis === "ancestors-or-self" || axis === "ancestors";
const walkDescendents = axis === "descendents-or-self" || axis === "descendents";
const walkSelf = axis === "ancestors-or-self" || axis === "self" || axis === "descendents-or-self";
@@ -274,7 +796,7 @@ namespace Utils {
if (entry) return entry;
}
if (walkAncestors) {
- const entry = !(this instanceof VirtualFileSystem) && this.parent.findEntry(path, "ancestors-or-self", options);
+ const entry = this.parent && this.parent.findEntry(path, "ancestors-or-self", options);
if (entry) return entry;
}
if (walkDescendents) {
@@ -285,345 +807,53 @@ namespace Utils {
}
}
- public findDirectory(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined {
+ /**
+ * Finds a directory for a relative path along the provided axis.
+ */
+ public findDirectory(path: string, axis: Axis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualDirectory | undefined {
return this.findEntry(path, axis, { ...options, kind: "directory" });
}
- public findFile(path: string, axis: FindAxis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined {
+ /**
+ * Finds a file for a relative path along the provided axis.
+ */
+ public findFile(path: string, axis: Axis, options: { followSymlinks?: boolean, pattern?: RegExp } = {}): VirtualFile | undefined {
return this.findEntry(path, axis, { ...options, kind: "file" });
}
- protected abstract getOwnEntries(): KeyedCollection;
-
- protected raiseOnChildAdded(entry: VirtualFile | VirtualDirectory) {
- if (this._childAddedCallbacks) {
- for (const callback of this._childAddedCallbacks) {
- callback(entry);
- }
- }
- }
-
- protected raiseOnChildRemoved(entry: VirtualFile | VirtualDirectory) {
- if (this._childRemovedCallbacks) {
- for (const callback of this._childRemovedCallbacks) {
- callback(entry);
- }
- }
- }
- }
-
- export class VirtualFileSystem extends VirtualFileSystemContainer {
- // private static _builtLocal: VirtualFileSystem | undefined;
- // private static _builtLocalCI: VirtualFileSystem | undefined;
- // private static _builtLocalCS: VirtualFileSystem | undefined;
-
- private _root: VirtualDirectory;
- private _useCaseSensitiveFileNames: boolean;
- private _currentDirectory: string;
- private _currentDirectoryStack: string[] | undefined;
- private _shadowRoot: VirtualFileSystem | undefined;
-
- constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) {
- super("");
- this._currentDirectory = currentDirectory.replace(/\\/g, "/");
- this._useCaseSensitiveFileNames = useCaseSensitiveFileNames;
- }
-
- public get fileSystem(): VirtualFileSystem {
- return this;
- }
-
- public get parent(): VirtualFileSystemContainer {
- return this;
- }
-
- public get shadowRoot(): VirtualFileSystem | undefined {
- return this._shadowRoot;
- }
-
- public get useCaseSensitiveFileNames() {
- return this._useCaseSensitiveFileNames;
- }
-
- public get currentDirectory() {
- return this._currentDirectory;
- }
-
- public get path() {
- return "";
- }
-
- public get relative() {
- return "";
- }
-
- public get exists() {
- return true;
- }
-
- public get root() {
- if (this._root === undefined) {
- if (this._shadowRoot) {
- this._root = this._shadowRoot.root.shadow(this);
- }
- else {
- this._root = new VirtualDirectory(this, "");
- }
- if (this.isReadOnly) this._root.makeReadOnly();
- }
- return this._root;
- }
-
- // public static getBuiltLocal(useCaseSensitiveFileNames: boolean = io.useCaseSensitiveFileNames()): VirtualFileSystem {
- // let vfs = useCaseSensitiveFileNames ? this._builtLocalCS : this._builtLocalCI;
- // if (!vfs) {
- // vfs = this._builtLocal;
- // if (!vfs) {
- // const resolver = createResolver(io, {
- // "/.ts": getBuiltDirectory(),
- // "/.lib": getLibFilesDirectory()
- // });
- // vfs = new VirtualFileSystem("/", io.useCaseSensitiveFileNames());
- // vfs.addDirectory("/.ts", resolver);
- // vfs.addDirectory("/.lib", resolver);
- // vfs.addDirectory("/.test");
- // vfs.changeDirectory("/.test");
- // vfs.makeReadOnly();
- // this._builtLocal = vfs;
- // }
- // if (vfs._useCaseSensitiveFileNames !== useCaseSensitiveFileNames) {
- // vfs = vfs.shadow();
- // vfs._useCaseSensitiveFileNames = useCaseSensitiveFileNames;
- // vfs.makeReadOnly();
- // }
- // return useCaseSensitiveFileNames
- // ? this._builtLocalCS = vfs
- // : this._builtLocalCI = vfs;
- // }
- // return vfs;
- // }
-
- // public static createFromOptions(options: { useCaseSensitiveFileNames?: boolean, currentDirectory?: string }) {
- // const vfs = this.getBuiltLocal(options.useCaseSensitiveFileNames).shadow();
- // if (options.currentDirectory) {
- // vfs.addDirectory(options.currentDirectory);
- // vfs.changeDirectory(options.currentDirectory);
- // }
- // return vfs;
- // }
-
- // public static createFromDocuments(options: { useCaseSensitiveFileNames?: boolean, currentDirectory?: string }, documents: TextDocument[], fileOptions?: { overwrite?: boolean }) {
- // const vfs = this.createFromOptions(options);
- // for (const document of documents) {
- // const file = vfs.addFile(document.file, document.text, fileOptions)!;
- // assert.isDefined(file, `Failed to add file: '${document.file}'`);
- // file.metadata.set("document", document);
- // // Add symlinks
- // const symlink = document.meta.get("symlink");
- // if (file && symlink) {
- // for (const link of symlink.split(",")) {
- // const symlink = vfs.addSymlink(vpath.resolve(vfs.currentDirectory, link.trim()), file)!;
- // assert.isDefined(symlink, `Failed to symlink: '${link}'`);
- // symlink.metadata.set("document", document);
- // }
- // }
- // }
- // return vfs;
- // }
-
- public changeDirectory(path: string) {
- this.writePreamble();
- if (path) {
- this._currentDirectory = vpath.resolve(this._currentDirectory, path);
- }
- }
-
- public pushDirectory(path = this.currentDirectory) {
- this.writePreamble();
- if (this._currentDirectoryStack === undefined) {
- this._currentDirectoryStack = [this.currentDirectory];
- }
- else {
- this._currentDirectoryStack.push(this.currentDirectory);
- }
- this.changeDirectory(path);
- }
-
- public popDirectory() {
- this.writePreamble();
- const previousDirectory = this._currentDirectoryStack && this._currentDirectoryStack.pop();
- if (previousDirectory !== undefined) {
- this._currentDirectory = previousDirectory;
- }
- }
-
- public addDirectory(path: string /*, resolver?: FileSystemResolver */) {
- return this.root.addDirectory(vpath.resolve(this.currentDirectory, path) /*, resolver */);
- }
-
- public addFile(path: string, content?: /*FileSystemResolver["getContent"] |*/ string, options?: { overwrite?: boolean }) {
- return this.root.addFile(vpath.resolve(this.currentDirectory, path), content, options);
- }
-
- public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined;
- public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined;
- public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined;
- public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory) {
- if (typeof target === "string") target = vpath.resolve(this.currentDirectory, target);
- return this.root.addSymlink(vpath.resolve(this.currentDirectory, path), target);
- }
-
- public removeDirectory(path: string): boolean {
- return this.root.removeDirectory(vpath.resolve(this.currentDirectory, path));
- }
-
- public removeFile(path: string): boolean {
- return this.root.removeFile(vpath.resolve(this.currentDirectory, path));
- }
-
- public readFile(path: string): string | undefined {
- const file = this.getFile(vpath.resolve(this.currentDirectory, path));
- return file && file.getContent();
- }
-
- public writeFile(path: string, content: string): void {
- path = vpath.resolve(this.currentDirectory, path);
- const file = this.getFile(path) || this.addFile(path);
- if (file) {
- file.setContent(content);
- }
- }
-
- public directoryExists(path: string) {
- return this.getEntry(path) instanceof VirtualDirectory;
- }
-
- public fileExists(path: string) {
- return this.getEntry(path) instanceof VirtualFile;
- }
-
- public sameName(a: string, b: string) {
- return compareStrings(a, b, !this.useCaseSensitiveFileNames) === 0;
- }
-
- public getRealEntry(entry: VirtualDirectory): VirtualDirectory | undefined;
- public getRealEntry(entry: VirtualFile): VirtualFile | undefined;
- public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined;
- public getRealEntry(entry: VirtualFile | VirtualDirectory): VirtualFile | VirtualDirectory | undefined {
- if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) {
- return findTarget(this, entry.target);
- }
- return entry;
- }
-
- public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
- public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
- public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined;
- public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) {
- return this.root.getEntry(vpath.resolve(this.currentDirectory, path), options);
- }
-
- public getFile(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualFile | undefined {
- return this.root.getFile(vpath.resolve(this.currentDirectory, path), options);
- }
-
- public getDirectory(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp }): VirtualDirectory | undefined {
- return this.root.getDirectory(vpath.resolve(this.currentDirectory, path), options);
- }
-
- public getAccessibleFileSystemEntries(path: string) {
- const entry = this.getEntry(path);
- if (entry instanceof VirtualDirectory) {
- return {
- files: entry.getFiles().map(f => f.name),
- directories: entry.getDirectories().map(d => d.name)
- };
- }
- return { files: [], directories: [] };
- }
-
- public shadow(): VirtualFileSystem {
- const fs = new VirtualFileSystem(this.currentDirectory, this.useCaseSensitiveFileNames);
- fs._shadowRoot = this;
- return fs;
- }
-
- public debugPrint(): void {
- console.log(`cwd: ${this.currentDirectory}`);
- for (const entry of this.getEntries({ recursive: true })) {
- if (entry instanceof VirtualDirectory) {
- console.log(entry.path.endsWith("/") ? entry.path : entry.path + "/");
- if (entry instanceof VirtualDirectorySymlink) {
- console.log(`-> ${entry.target.endsWith("/") ? entry.target : entry.target + "/"}`);
- }
- }
- else {
- console.log(entry.path);
- if (entry instanceof VirtualFileSymlink) {
- console.log(`-> ${entry.target}`);
- }
- }
- }
- }
-
- protected makeReadOnlyCore() {
- this.root.makeReadOnly();
- }
-
- protected getOwnEntries() {
- return this.root["getOwnEntries"]();
- }
- }
-
- export class VirtualDirectory extends VirtualFileSystemContainer {
- protected _shadowRoot: VirtualDirectory | undefined;
- private _parent: VirtualFileSystemContainer;
- private _entries: KeyedCollection | undefined;
- // private _resolver: FileSystemResolver | undefined;
-
- constructor(parent: VirtualFileSystemContainer, name: string /*, resolver?: FileSystemResolver */) {
- super(name);
- this._parent = parent;
- this._entries = undefined;
- // this._resolver = resolver;
- this._shadowRoot = undefined;
- }
-
- public get parent(): VirtualFileSystemContainer {
- return this._parent;
- }
-
- public get shadowRoot(): VirtualDirectory | undefined {
- return this._shadowRoot;
- }
-
- public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
- public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
- public getEntry(path: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined;
- public getEntry(path: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined {
- const components = this.parsePath(path);
- const directory = this.walkContainers(components, /*create*/ false);
- return directory && directory.getOwnEntry(components[components.length - 1], options);
- }
-
- public addDirectory(path: string /*, resolver?: FileSystemResolver*/): VirtualDirectory | undefined {
+ /**
+ * Adds a directory (and all intermediate directories) for a path relative to this directory.
+ */
+ public addDirectory(path: string, resolver?: FileSystemResolver): VirtualDirectory | undefined {
this.writePreamble();
const components = this.parsePath(path);
const directory = this.walkContainers(components, /*create*/ true);
- return directory && directory.addOwnDirectory(components[components.length - 1] /*, resolver*/);
+ return directory && directory.addOwnDirectory(components[components.length - 1], resolver);
}
- public addFile(path: string, content?: /*FileSystemResolver["getContent"] |*/ string | undefined, options?: { overwrite?: boolean }): VirtualFile | undefined {
+ /**
+ * Adds a file (and all intermediate directories) for a path relative to this directory.
+ */
+ public addFile(path: string, content?: FileSystemResolver | ContentResolver | string, options?: { overwrite?: boolean }): VirtualFile | undefined {
this.writePreamble();
const components = this.parsePath(path);
const directory = this.walkContainers(components, /*create*/ true);
return directory && directory.addOwnFile(components[components.length - 1], content, options);
}
+ /**
+ * Adds a symbolic link to a target entry for a path relative to this directory.
+ */
public addSymlink(path: string, target: VirtualFile): VirtualFileSymlink | undefined;
+ /**
+ * Adds a symbolic link to a target entry for a path relative to this directory.
+ */
public addSymlink(path: string, target: VirtualDirectory): VirtualDirectorySymlink | undefined;
- public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined;
- public addSymlink(path: string, target: string | VirtualFile | VirtualDirectory): VirtualSymlink | undefined {
+ /**
+ * Adds a symbolic link to a target entry for a path relative to this directory.
+ */
+ public addSymlink(path: string, target: string | VirtualEntry): VirtualSymlink | undefined;
+ public addSymlink(path: string, target: string | VirtualEntry): VirtualSymlink | undefined {
this.writePreamble();
const targetEntry = typeof target === "string" ? this.fileSystem.getEntry(vpath.resolve(this.path, target)) : target;
if (targetEntry === undefined) return undefined;
@@ -632,6 +862,9 @@ namespace Utils {
return directory && directory.addOwnSymlink(components[components.length - 1], targetEntry);
}
+ /**
+ * Removes a directory (and all of its contents) at a path relative to this directory.
+ */
public removeDirectory(path: string): boolean {
this.writePreamble();
const components = this.parsePath(path);
@@ -639,6 +872,9 @@ namespace Utils {
return directory ? directory.removeOwnDirectory(components[components.length - 1]) : false;
}
+ /**
+ * Removes a file at a path relative to this directory.
+ */
public removeFile(path: string): boolean {
this.writePreamble();
this.writePreamble();
@@ -647,9 +883,13 @@ namespace Utils {
return directory ? directory.removeOwnFile(components[components.length - 1]) : false;
}
- public shadow(parent: VirtualFileSystemContainer): VirtualDirectory {
- this.shadowPreamble(parent);
- const shadow = new VirtualDirectory(parent, this.name);
+ /**
+ * Creates a shadow copy of this directory. Changes made to the shadow do not affect
+ * this directory.
+ */
+ public shadow(shadowParent: VirtualDirectory): VirtualDirectory {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualDirectory(shadowParent, this.name);
shadow._shadowRoot = this;
return shadow;
}
@@ -662,10 +902,11 @@ namespace Utils {
protected getOwnEntries() {
if (!this._entries) {
- // const resolver = this._resolver;
- const entries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive);
- // this._resolver = undefined;
- /*if (resolver) {
+ const entries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive);
+ const resolver = this._resolver;
+ const shadowRoot = this._shadowRoot;
+ if (resolver) {
+ this._resolver = undefined;
const { files, directories } = resolver.getEntries(this);
for (const dir of directories) {
const vdir = new VirtualDirectory(this, dir, resolver);
@@ -673,14 +914,14 @@ namespace Utils {
entries.set(vdir.name, vdir);
}
for (const file of files) {
- const vfile = new VirtualFile(this, file, file => resolver.getContent(file));
+ const vfile = new VirtualFile(this, file, resolver);
if (this.isReadOnly) vfile.makeReadOnly();
entries.set(vfile.name, vfile);
}
}
- else*/ if (this._shadowRoot) {
- this._shadowRoot.getOwnEntries().forEach(entry => {
- const clone = (entry).shadow(this);
+ else if (shadowRoot) {
+ shadowRoot.getOwnEntries().forEach(entry => {
+ const clone = entry.shadow(this);
if (this.isReadOnly) clone.makeReadOnly();
entries.set(clone.name, clone);
});
@@ -690,59 +931,24 @@ namespace Utils {
return this._entries;
}
- private parsePath(path: string) {
- return vpath.parse(vpath.normalize(path));
- }
-
- private walkContainers(components: string[], create: boolean) {
- // no absolute paths (unless this is the root)
- if (!!components[0] === !(this.parent instanceof VirtualFileSystem)) return undefined;
-
- // no relative paths
- if (components[1] === "..") return undefined;
-
- // walk the components
- let directory: VirtualDirectory | undefined = this;
- for (let i = this.parent instanceof VirtualFileSystem ? 0 : 1; i < components.length - 1; i++) {
- directory = create ? directory.getOrAddOwnDirectory(components[i]) : directory.getOwnDirectory(components[i]);
- if (directory === undefined) return undefined;
- }
-
- return directory;
- }
-
- private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
- private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
- private getOwnEntry(name: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualFile | VirtualDirectory | undefined;
- private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualFile | VirtualDirectory | undefined {
- const entry = this.getOwnEntries().get(name);
- return entry && isMatch(entry, options) ? options.followSymlinks ? this.fileSystem.getRealEntry(entry) : entry : undefined;
- }
-
- private getOwnDirectory(name: string) {
- return this.getOwnEntry(name, { kind: "directory" });
- }
-
- private getOrAddOwnDirectory(name: string) {
- return this.getOwnDirectory(name) || this.addOwnDirectory(name);
- }
-
- private addOwnDirectory(name: string /*, resolver?: FileSystemResolver */): VirtualDirectory | undefined {
+ protected addOwnDirectory(name: string, resolver?: FileSystemResolver): VirtualDirectory | undefined {
const existing = this.getOwnEntry(name);
if (existing) {
- if (/*!resolver &&*/ existing instanceof VirtualDirectory) {
+ if (!resolver && existing instanceof VirtualDirectory) {
return existing;
}
return undefined;
}
- const entry = new VirtualDirectory(this, name /*, resolver */);
+ const entry = new VirtualDirectory(this, name, resolver);
this.getOwnEntries().set(entry.name, entry);
- this.raiseOnChildAdded(entry);
+ this.emit("childAdded", entry);
+ entry.emit("fileSystemChange", entry.path, "added");
+ entry.addListener("fileSystemChange", this._onChildFileSystemChange);
return entry;
}
- private addOwnFile(name: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined, options: { overwrite?: boolean } = {}): VirtualFile | undefined {
+ protected addOwnFile(name: string, content?: FileSystemResolver | ContentResolver | string, options: { overwrite?: boolean } = {}): VirtualFile | undefined {
const existing = this.getOwnEntry(name);
if (existing) {
if (!options.overwrite || !(existing instanceof VirtualFile)) {
@@ -755,177 +961,322 @@ namespace Utils {
const entry = new VirtualFile(this, name, content);
this.getOwnEntries().set(entry.name, entry);
- this.raiseOnChildAdded(entry);
+ this.emit("childAdded", entry);
+ entry.emit("fileSystemChange", entry.path, "added");
+ entry.addListener("fileSystemChange", this._onChildFileSystemChange);
return entry;
}
- private addOwnSymlink(name: string, target: VirtualFile | VirtualDirectory): VirtualSymlink | undefined {
+ protected addOwnSymlink(name: string, target: VirtualEntry): VirtualSymlink | undefined {
if (this.getOwnEntry(name)) return undefined;
const entry = target instanceof VirtualFile ? new VirtualFileSymlink(this, name, target.path) : new VirtualDirectorySymlink(this, name, target.path);
this.getOwnEntries().set(entry.name, entry);
- this.raiseOnChildAdded(entry);
+ this.emit("childAdded", entry);
+ entry.emit("fileSystemChange", entry.path, "added");
+ entry.addListener("fileSystemChange", this._onChildFileSystemChange);
return entry;
}
- private removeOwnDirectory(name: string) {
+ protected removeOwnDirectory(name: string) {
const entries = this.getOwnEntries();
const entry = entries.get(name);
if (entry instanceof VirtualDirectory) {
entries.delete(name);
- this.raiseOnChildRemoved(entry);
+ this.emit("childRemoved", entry);
+ this.emit("fileSystemChange", entry.path, "removed");
+ entry.removeListener("fileSystemChange", this._onChildFileSystemChange);
return true;
}
return false;
}
- private removeOwnFile(name: string) {
+ protected removeOwnFile(name: string) {
const entries = this.getOwnEntries();
const entry = entries.get(name);
if (entry instanceof VirtualFile) {
entries.delete(name);
- this.raiseOnChildRemoved(entry);
+ this.emit("childRemoved", entry);
+ this.emit("fileSystemChange", entry.path, "removed");
+ entry.removeListener("fileSystemChange", this._onChildFileSystemChange);
return true;
}
return false;
}
+
+ private parsePath(path: string) {
+ return vpath.parse(vpath.normalize(path));
+ }
+
+ private walkContainers(components: string[], create: boolean) {
+ // no absolute paths (unless this is the root)
+ if (!!components[0] === !!this.parent) return undefined;
+
+ // no relative paths
+ if (components[1] === "..") return undefined;
+
+ // walk the components
+ let directory: VirtualDirectory | undefined = this;
+ for (let i = this.parent ? 1 : 0; i < components.length - 1; i++) {
+ directory = create ? directory.getOrAddOwnDirectory(components[i]) : directory.getOwnDirectory(components[i]);
+ if (directory === undefined) return undefined;
+ }
+
+ return directory;
+ }
+
+ private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile | undefined;
+ private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory | undefined;
+ private getOwnEntry(name: string, options?: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry | undefined;
+ private getOwnEntry(name: string, options: { followSymlinks?: boolean, pattern?: RegExp, kind?: "file" | "directory" } = {}): VirtualEntry | undefined {
+ const entry = this.getOwnEntries().get(name);
+ return entry && isMatch(entry, options) ? options.followSymlinks ? this.fileSystem.getRealEntry(entry) : entry : undefined;
+ }
+
+ private getOwnDirectory(name: string) {
+ return this.getOwnEntry(name, { kind: "directory" });
+ }
+
+ private getOrAddOwnDirectory(name: string) {
+ return this.getOwnDirectory(name) || this.addOwnDirectory(name);
+ }
+
+ private onChildFileSystemChange(path: string, change: FileSystemChange) {
+ this.emit("fileSystemChange", path, change);
+ }
}
export class VirtualDirectorySymlink extends VirtualDirectory {
private _targetPath: string;
private _target: VirtualDirectory | undefined;
- private _symLinks = new Map();
- private _symEntries: KeyedCollection | undefined;
- private _onTargetParentChildRemoved: (entry: VirtualFile | VirtualDirectory) => void;
- private _onTargetChildRemoved: (entry: VirtualFile | VirtualDirectory) => void;
- private _onTargetChildAdded: (entry: VirtualFile | VirtualDirectory) => void;
+ private _pullEntries: KeyedCollection | undefined;
+ private _allEntries: KeyedCollection | undefined;
+ private _onTargetParentChildRemoved: (entry: VirtualEntry) => void;
+ private _onTargetChildRemoved: (entry: VirtualEntry) => void;
+ private _onTargetChildAdded: (entry: VirtualEntry) => void;
+ private _onTargetFileSystemChange: (path: string, change: FileSystemChange) => void;
- constructor(parent: VirtualFileSystemContainer, name: string, target: string) {
+ constructor(parent: VirtualDirectory, name: string, target: string) {
super(parent, name);
+ this._pullEntries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive);
this._targetPath = target;
this._onTargetParentChildRemoved = entry => this.onTargetParentChildRemoved(entry);
this._onTargetChildAdded = entry => this.onTargetChildAdded(entry);
this._onTargetChildRemoved = entry => this.onTargetChildRemoved(entry);
+ this._onTargetFileSystemChange = (path, change) => this.onTargetFileSystemChange(path, change);
}
- public get target() {
+ /**
+ * Gets the path to the target of the symbolic link.
+ */
+ public get targetPath() {
return this._targetPath;
}
- public set target(value: string) {
- this.writePreamble();
+ /**
+ * Sets the path to the target of the symbolic link.
+ */
+ public set targetPath(value: string) {
if (this._targetPath !== value) {
- this._targetPath = value;
+ this.writePreamble();
+ this._targetPath = vpath.resolve(this.path, value);
this.invalidateTarget();
}
}
- public get isBroken(): boolean {
- return this.getRealDirectory() === undefined;
- }
-
- public getRealDirectory(): VirtualDirectory | undefined {
+ /**
+ * Gets the resolved target directory for this symbolic link.
+ */
+ public get target(): VirtualDirectory | undefined {
this.resolveTarget();
return this._target;
}
- public addDirectory(path: string /*, resolver?: FileSystemResolver*/): VirtualDirectory | undefined {
- const target = this.getRealDirectory();
- return target && target.addDirectory(path /*, resolver*/);
+ /**
+ * Gets a value indicating whether the symbolic link is broken.
+ */
+ public get isBroken(): boolean {
+ return this.target === undefined;
}
- public addFile(path: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined): VirtualFile | undefined {
- const target = this.getRealDirectory();
- return target && target.addFile(path, content);
- }
-
- public removeDirectory(path: string): boolean {
- const target = this.getRealDirectory();
- return target && target.removeDirectory(path) || false;
- }
-
- public removeFile(path: string): boolean {
- const target = this.getRealDirectory();
- return target && target.removeFile(path) || false;
- }
-
- public shadow(parent: VirtualFileSystemContainer): VirtualDirectorySymlink {
- this.shadowPreamble(parent);
- const shadow = new VirtualDirectorySymlink(parent, this.name, this.target);
+ /**
+ * Creates a shadow copy of this directory. Changes made to the shadow do not affect
+ * this directory.
+ */
+ public shadow(shadowParent: VirtualDirectory): VirtualDirectorySymlink {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualDirectorySymlink(shadowParent, this.name, this.targetPath);
shadow._shadowRoot = this;
return shadow;
}
- public resolveTarget(): void {
- if (!this._target) {
- const entry = findTarget(this.fileSystem, this.target);
- if (entry instanceof VirtualDirectory) {
- this._target = entry;
- this._target.parent.addOnChildRemoved(this._onTargetParentChildRemoved);
- this._target.addOnChildAdded(this._onTargetChildAdded);
- this._target.addOnChildRemoved(this._onTargetChildRemoved);
- }
- }
+ protected addOwnDirectory(name: string, resolver?: FileSystemResolver): VirtualDirectory | undefined {
+ const realTarget = this.target;
+ const child = realTarget && realTarget.addDirectory(name, resolver);
+ return child && this.getView(child);
}
- protected getOwnEntries(): KeyedCollection {
- if (!this._symEntries) {
- const target = this.getRealDirectory();
- this._symEntries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive);
- if (target) {
- for (const entry of target.getEntries()) {
- this._symEntries.set(entry.name, this.getWrappedEntry(entry));
+ protected addOwnFile(name: string, content?: FileSystemResolver | ContentResolver | string): VirtualFile | undefined {
+ const realTarget = this.target;
+ const child = realTarget && realTarget.addFile(name, content);
+ return child && this.getView(child);
+ }
+
+ protected addOwnSymlink(name: string, target: VirtualEntry): VirtualSymlink | undefined {
+ const realTarget = this.target;
+ const child = realTarget && realTarget.addSymlink(name, target);
+ return child && this.getView(child);
+ }
+
+ protected removeOwnDirectory(name: string): boolean {
+ const realTarget = this.target;
+ return realTarget && realTarget.removeDirectory(name) || false;
+ }
+
+ protected removeOwnFile(name: string): boolean {
+ const realTarget = this.target;
+ return realTarget && realTarget.removeFile(name) || false;
+ }
+
+ protected getOwnEntries(): KeyedCollection {
+ if (!this._allEntries) {
+ const realTarget = this.target;
+ this._allEntries = new KeyedCollection(this.fileSystem.useCaseSensitiveFileNames ? compareStrings.caseSensitive : compareStrings.caseInsensitive);
+ if (realTarget) {
+ for (const entry of realTarget.getEntries()) {
+ this._allEntries.set(entry.name, this.getView(entry));
}
}
}
- return this._symEntries;
+ return this._allEntries;
}
- private getWrappedEntry(entry: VirtualFile | VirtualDirectory) {
- let symlink = this._symLinks.get(entry);
+ private getView(entry: VirtualFile): VirtualFileView;
+ private getView(entry: VirtualDirectory): VirtualDirectoryView;
+ private getView(entry: VirtualEntry): VirtualEntryView;
+ private getView(entry: VirtualEntry) {
+ let symlink = this._pullEntries.get(entry.name);
if (entry instanceof VirtualFile) {
- if (symlink instanceof VirtualFileSymlink) {
+ if (symlink instanceof VirtualFileView) {
return symlink;
}
- symlink = new VirtualFileSymlink(this, entry.name, entry.path);
- this._symLinks.set(entry, symlink);
+ symlink = new VirtualFileView(this, entry.name, entry.path);
+ this._pullEntries.set(entry.name, symlink);
}
else {
- if (symlink instanceof VirtualDirectorySymlink) {
+ if (symlink instanceof VirtualDirectoryView) {
return symlink;
}
- symlink = new VirtualDirectorySymlink(this, entry.name, entry.path);
- this._symLinks.set(entry, symlink);
+ symlink = new VirtualDirectoryView(this, entry.name, entry.path);
+ this._pullEntries.set(entry.name, symlink);
}
return symlink;
}
- private onTargetParentChildRemoved(entry: VirtualFileSystemEntry) {
- if (entry !== this._target) return;
- this.invalidateTarget();
- }
-
- private onTargetChildAdded(entry: VirtualFile | VirtualDirectory) {
- const wrapped = this.getWrappedEntry(entry);
- this.getOwnEntries().set(entry.name, wrapped);
- this.raiseOnChildAdded(wrapped);
- }
-
- private onTargetChildRemoved(entry: VirtualFile | VirtualDirectory) {
- const wrapped = this.getWrappedEntry(entry);
- this.getOwnEntries().delete(entry.name);
- this._symLinks.delete(entry);
- this.raiseOnChildRemoved(wrapped);
+ private resolveTarget(): void {
+ if (!this._target) {
+ const entry = findTarget(this.fileSystem, this.targetPath);
+ if (entry instanceof VirtualDirectory) {
+ this._target = entry;
+ if (this._target.parent) this._target.parent.addListener("childRemoved", this._onTargetParentChildRemoved);
+ this._target.addListener("childAdded", this._onTargetChildAdded);
+ this._target.addListener("childRemoved", this._onTargetChildRemoved);
+ this._target.addListener("fileSystemChange", this._onTargetFileSystemChange);
+ }
+ }
}
private invalidateTarget() {
if (!this._target) return;
- this._target.parent.removeOnChildRemoved(this._onTargetParentChildRemoved);
- this._target.removeOnChildAdded(this._onTargetChildAdded);
- this._target.removeOnChildRemoved(this._onTargetChildRemoved);
+ if (this._target.parent) this._target.parent.removeListener("childRemoved", this._onTargetParentChildRemoved);
+ this._target.removeListener("childAdded", this._onTargetChildAdded);
+ this._target.removeListener("childRemoved", this._onTargetChildRemoved);
+ this._target.removeListener("fileSystemChange", this._onTargetFileSystemChange);
this._target = undefined;
- this._symLinks.clear();
- this._symEntries = undefined;
+ this._pullEntries.clear();
+ this._allEntries = undefined;
}
+
+ private onTargetParentChildRemoved(entry: VirtualEntry) {
+ if (entry === this._target) {
+ this.invalidateTarget();
+ }
+ }
+
+ private onTargetChildAdded(entry: VirtualEntry) {
+ const wrapped = this.getView(entry);
+ this.getOwnEntries().set(entry.name, wrapped);
+ this.emit("childAdded", wrapped);
+ }
+
+ private onTargetChildRemoved(entry: VirtualEntry) {
+ const symlink = this.getView(entry);
+ this.getOwnEntries().delete(entry.name);
+ this._pullEntries.delete(entry.name);
+ this.emit("childRemoved", symlink);
+ }
+
+ protected onTargetFileSystemChange(path: string, change: FileSystemChange) {
+ const ignoreCase = !this.fileSystem.useCaseSensitiveFileNames;
+ if (vpath.beneath(this.targetPath, path, ignoreCase)) {
+ const relative = vpath.relative(this.targetPath, path, ignoreCase);
+ const symbolicPath = vpath.combine(this.path, relative);
+ this.emit("fileSystemChange", symbolicPath, change);
+ }
+ }
+ }
+
+ class VirtualDirectoryView extends VirtualDirectorySymlink {
+ /**
+ * Creates a shadow copy of this directory. Changes made to the shadow do not affect
+ * this directory.
+ */
+ public shadow(shadowParent: VirtualDirectory): VirtualDirectoryView {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualDirectoryView(shadowParent, this.name, this.targetPath);
+ shadow._shadowRoot = this;
+ return shadow;
+ }
+
+ protected onTargetFileSystemChange() { /* views do not propagate file system events */ }
+ }
+
+ class VirtualRoot extends VirtualDirectory {
+ private _fileSystem: VirtualFileSystem;
+
+ constructor(fileSystem: VirtualFileSystem) {
+ super(/*parent*/ undefined, "");
+ this._fileSystem = fileSystem;
+ }
+
+ public get fileSystem(): VirtualFileSystem {
+ return this._fileSystem;
+ }
+
+ public get path(): string {
+ return "";
+ }
+
+ public get exists(): boolean {
+ return true;
+ }
+
+ public _shadow(shadowFileSystem: VirtualFileSystem) {
+ super.checkShadowFileSystem(shadowFileSystem);
+ const shadow = new VirtualRoot(shadowFileSystem);
+ shadow._shadowRoot = this;
+ return shadow;
+ }
+
+ public shadow(): never {
+ throw new TypeError();
+ }
+ }
+
+ export interface VirtualFile {
+ on(event: "fileSystemChange", listener: (path: string, change: FileSystemChange) => void): this;
+ on(event: "contentChanged", listener: (entry: VirtualFile) => void): this;
+ emit(event: "fileSystemChange", path: string, change: FileSystemChange): boolean;
+ emit(event: "contentChanged", entry: VirtualFile): boolean;
}
export class VirtualFile extends VirtualFileSystemEntry {
@@ -933,51 +1284,72 @@ namespace Utils {
private _parent: VirtualDirectory;
private _content: string | undefined;
private _contentWasSet: boolean;
- // private _resolver: FileSystemResolver["getContent"] | undefined;
+ private _resolver: FileSystemResolver | ContentResolver | undefined;
- constructor(parent: VirtualDirectory, name: string, content?: /*FileSystemResolver["getContent"]*/ | string | undefined) {
+ constructor(parent: VirtualDirectory, name: string, content?: FileSystemResolver | ContentResolver | string) {
super(name);
this._parent = parent;
this._content = typeof content === "string" ? content : undefined;
- // this._resolver = typeof content === "function" ? content : undefined;
+ this._resolver = typeof content !== "string" ? content : undefined;
this._shadowRoot = undefined;
this._contentWasSet = this._content !== undefined;
}
+ /**
+ * Gets the parent directory for this entry.
+ */
public get parent(): VirtualDirectory {
return this._parent;
}
+ /**
+ * Gets the entry that this entry shadows.
+ */
public get shadowRoot(): VirtualFile | undefined {
return this._shadowRoot;
}
- public getContent(): string | undefined {
+ /**
+ * Gets the text content of this file.
+ */
+ public get content(): string | undefined {
if (!this._contentWasSet) {
- // const resolver = this._resolver;
+ const resolver = this._resolver;
const shadowRoot = this._shadowRoot;
- /* if (resolver) {
- this._content = resolver(this);
+ if (resolver) {
+ this._resolver = undefined;
+ this._content = typeof resolver === "function" ? resolver(this) : resolver.getContent(this);
this._contentWasSet = true;
}
- else */ if (shadowRoot) {
- this._content = shadowRoot.getContent();
+ else if (shadowRoot) {
+ this._content = shadowRoot.content;
this._contentWasSet = true;
}
}
return this._content;
}
- public setContent(value: string | undefined) {
- this.writePreamble();
- // this._resolver = undefined;
- this._content = value;
- this._contentWasSet = true;
+ /**
+ * Sets the text content of this file.
+ */
+ public set content(value: string | undefined) {
+ if (this.content !== value) {
+ this.writePreamble();
+ this._resolver = undefined;
+ this._content = value;
+ this._contentWasSet = true;
+ this.emit("contentChanged", this);
+ this.emit("fileSystemChange", this.path, "modified");
+ }
}
- public shadow(parent: VirtualDirectory): VirtualFile {
- this.shadowPreamble(parent);
- const shadow = new VirtualFile(parent, this.name);
+ /**
+ * Creates a shadow copy of this file. Changes made to the shadow do not affect
+ * this file.
+ */
+ public shadow(shadowParent: VirtualDirectory): VirtualFile {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualFile(shadowParent, this.name);
shadow._shadowRoot = this;
shadow._contentWasSet = false;
return shadow;
@@ -988,69 +1360,150 @@ namespace Utils {
}
export class VirtualFileSymlink extends VirtualFile {
- private _target: string;
+ private _targetPath: string;
+ private _target: VirtualFile | undefined;
+ private _onTargetParentChildRemoved: (entry: VirtualEntry) => void;
+ private _onTargetContentChanged: () => void;
+ private _onTargetFileSystemChange: (path: string, change: FileSystemChange) => void;
constructor(parent: VirtualDirectory, name: string, target: string) {
super(parent, name);
- this._target = target;
+ this._targetPath = target;
+ this._onTargetParentChildRemoved = entry => this.onTargetParentChildRemoved(entry);
+ this._onTargetContentChanged = () => this.onTargetContentChanged();
+ this._onTargetFileSystemChange = (path, change) => this.onTargetFileSystemChange(path, change);
}
- public get target(): string {
+ /**
+ * Gets the path to the target of the symbolic link.
+ */
+ public get targetPath(): string {
+ return this._targetPath;
+ }
+
+ /**
+ * Sets the path to the target of the symbolic link.
+ */
+ public set targetPath(value: string) {
+ if (this._targetPath !== value) {
+ this.writePreamble();
+ this._targetPath = vpath.resolve(this.path, value);
+ this.invalidateTarget();
+ }
+ }
+
+ /**
+ * Gets the resolved target file for this symbolic link.
+ */
+ public get target(): VirtualFile | undefined {
+ this.resolveTarget();
return this._target;
}
- public set target(value: string) {
- this.writePreamble();
- this._target = value;
- }
-
+ /**
+ * Gets a value indicating whether the symbolic link is broken.
+ */
public get isBroken(): boolean {
- return this.getRealFile() === undefined;
+ return this.target === undefined;
}
- public getRealFile(): VirtualFile | undefined {
- const entry = findTarget(this.fileSystem, this.target);
- return entry instanceof VirtualFile ? entry : undefined;
+ /**
+ * Gets the text content of this file.
+ */
+ public get content(): string | undefined {
+ const realTarget = this.target;
+ return realTarget && realTarget.content;
}
- public getContent(): string | undefined {
- const target = this.getRealFile();
- return target && target.getContent();
+ /**
+ * Sets the text content of this file.
+ */
+ public set content(value: string | undefined) {
+ const realTarget = this.target;
+ if (realTarget) realTarget.content = value;
}
- public setContent(value: string | undefined) {
- const target = this.getRealFile();
- if (target) target.setContent(value);
- }
-
- public shadow(parent: VirtualDirectory) {
- this.shadowPreamble(parent);
- const shadow = new VirtualFileSymlink(parent, this.name, this.target);
+ /**
+ * Creates a shadow copy of this file. Changes made to the shadow do not affect
+ * this file.
+ */
+ public shadow(shadowParent: VirtualDirectory) {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualFileSymlink(shadowParent, this.name, this.targetPath);
shadow._shadowRoot = this;
return shadow;
}
+
+ private resolveTarget() {
+ if (!this._target) {
+ const entry = findTarget(this.fileSystem, this.targetPath);
+ if (entry instanceof VirtualFile) {
+ this._target = entry;
+ if (this._target.parent) this._target.parent.addListener("childRemoved", this._onTargetParentChildRemoved);
+ this._target.addListener("contentChanged", this._onTargetContentChanged);
+ this._target.addListener("fileSystemChange", this._onTargetFileSystemChange);
+ }
+ }
+ }
+
+ private invalidateTarget() {
+ if (!this._target) return;
+ if (this._target.parent) this._target.parent.removeListener("childRemoved", this._onTargetParentChildRemoved);
+ this._target.removeListener("contentChanged", this._onTargetContentChanged);
+ this._target.removeListener("fileSystemChange", this._onTargetFileSystemChange);
+ this._target = undefined;
+ }
+
+ private onTargetParentChildRemoved(entry: VirtualEntry) {
+ if (entry === this._target) {
+ this.invalidateTarget();
+ }
+ }
+
+ private onTargetContentChanged() {
+ this.emit("contentChanged", this);
+ }
+
+ protected onTargetFileSystemChange(_path: string, change: FileSystemChange) {
+ this.emit("fileSystemChange", this.path, change);
+ }
}
- export type VirtualSymlink = VirtualDirectorySymlink | VirtualFileSymlink;
+ class VirtualFileView extends VirtualFileSymlink {
+ /**
+ * Creates a shadow copy of this file. Changes made to the shadow do not affect
+ * this file.
+ */
+ public shadow(shadowParent: VirtualDirectory) {
+ this.shadowPreamble(shadowParent);
+ const shadow = new VirtualFileView(shadowParent, this.name, this.targetPath);
+ shadow._shadowRoot = this;
+ return shadow;
+ }
- function findTarget(vfs: VirtualFileSystem, target: string, set?: Set): VirtualFile | VirtualDirectory | undefined {
+ protected onTargetFileSystemChange() { /* views do not propagate file system events */ }
+ }
+
+ function findTarget(vfs: VirtualFileSystem, target: string, set?: Set): VirtualEntry | undefined {
const entry = vfs.getEntry(target);
if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) {
- if (!set) set = new Set();
+ if (!set) set = new Set();
if (set.has(entry)) return undefined;
set.add(entry);
- return findTarget(vfs, entry.target, set);
+ return findTarget(vfs, entry.targetPath, set);
}
return entry;
}
- function isMatch(entry: VirtualFile | VirtualDirectory, options: { pattern?: RegExp, kind?: "file" | "directory" }) {
+ function isMatch(entry: VirtualEntry, options: { pattern?: RegExp, kind?: "file" | "directory" }) {
return (options.pattern === undefined || options.pattern.test(entry.name))
&& (options.kind !== (entry instanceof VirtualFile ? "directory" : "file"));
}
+}
+namespace Utils {
// TODO(rbuckton): Move or retire this.
- export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost {
+ export class MockParseConfigHost extends vfs.VirtualFileSystem implements ts.ParseConfigHost {
constructor(currentDirectory: string, ignoreCase: boolean, files: ts.Map | string[]) {
super(currentDirectory, ignoreCase);
if (files instanceof Array) {
diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts
index c16f57235e4..0175f2a77cb 100644
--- a/src/harness/virtualFileSystemWithWatch.ts
+++ b/src/harness/virtualFileSystemWithWatch.ts
@@ -1,5 +1,7 @@
///
+// TODO(rbuckton): Migrate this to use vfs.
+
namespace ts.TestFSWithWatch {
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
export const libFile: FileOrFolder = {
diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts
index b2e98a4859f..44783ccd229 100644
--- a/src/harness/vpath.ts
+++ b/src/harness/vpath.ts
@@ -1,10 +1,16 @@
///
-
namespace vpath {
- import compareStrings = ts.compareStrings;
+ // NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been added
+ // to reduce the number of direct dependencies on compiler and services to eventually break away
+ // from depending directly on the compiler to speed up compilation time.
- export function normalizeSlashes(path: string): string {
- return path.replace(/\s*[\\/]\s*/g, "/").trim();
+ import compareValues = collections.compareValues;
+ import compareStrings = collections.compareStrings;
+
+ export const sep = "/";
+
+ export function normalizeSeparators(path: string): string {
+ return path.replace(/\s*[\\/]\s*/g, sep).trim();
}
const rootRegExp = /^[\\/]([\\/](.*?[\\/](.*?[\\/])?)?)?|^[a-zA-Z]:[\\/]?|^\w+:\/{2}[^\\/]*\/?/;
@@ -20,10 +26,18 @@ namespace vpath {
const trailingSeperatorRegExp = /[\\/]$/;
- export function hasTrailingSeperator(path: string) {
+ export function hasTrailingSeparator(path: string) {
return trailingSeperatorRegExp.test(path);
}
+ export function addTrailingSeparator(path: string) {
+ return hasTrailingSeparator(path) ? path : path + "/";
+ }
+
+ export function removeTrailingSeparator(path: string) {
+ return hasTrailingSeparator(path) ? path.slice(0, -1) : path;
+ }
+
function reduce(components: string[]) {
const normalized = [components[0]];
for (let i = 1; i < components.length; i++) {
@@ -41,20 +55,16 @@ namespace vpath {
export function normalize(path: string): string {
const components = reduce(parse(path));
- return components.length > 1 && hasTrailingSeperator(path) ? format(components) + "/" : format(components);
+ return components.length > 1 && hasTrailingSeparator(path) ? format(components) + sep : format(components);
}
export function combine(path: string, ...paths: string[]) {
- path = normalizeSlashes(path);
+ path = normalizeSeparators(path);
for (let name of paths) {
- name = normalizeSlashes(name);
+ name = normalizeSeparators(name);
if (name.length === 0) continue;
- if (path.length === 0 || isAbsolute(name)) {
- path = name;
- }
- else {
- path = hasTrailingSeperator(path) ? path + name : path + "/" + name;
- }
+ path = path.length === 0 || isAbsolute(name) ? name :
+ addTrailingSeparator(path) + name;
}
return path;
}
@@ -89,25 +99,65 @@ namespace vpath {
return format(["", ...components]);
}
+ export namespace relative {
+ export function caseSensitive(from: string, to: string) { return relative(from, to, /*ignoreCase*/ false); }
+ export function caseInsensitive(from: string, to: string) { return relative(from, to, /*ignoreCase*/ true); }
+ }
+
+ export function compare(a: string, b: string, ignoreCase: boolean) {
+ if (!isAbsolute(a)) throw new Error("Path not absolute");
+ if (!isAbsolute(b)) throw new Error("Path not absolute");
+ if (a === b) return 0;
+ a = removeTrailingSeparator(a);
+ b = removeTrailingSeparator(b);
+ if (a === b) return 0;
+ const aComponents = reduce(parse(a));
+ const bComponents = reduce(parse(b));
+ const len = Math.min(aComponents.length, bComponents.length);
+ for (let i = 0; i < len; i++) {
+ const result = compareStrings(aComponents[i], bComponents[i], ignoreCase);
+ if (result !== 0) return result;
+ }
+ return compareValues(aComponents.length, bComponents.length);
+ }
+
+ export namespace compare {
+ export function caseSensitive(a: string, b: string) { return compare(a, b, /*ignoreCase*/ false); }
+ export function caseInsensitive(a: string, b: string) { return compare(a, b, /*ignoreCase*/ true); }
+ }
+
+ export function equals(a: string, b: string, ignoreCase: boolean) {
+ return compare(a, b, ignoreCase) === 0;
+ }
+
+ export namespace equals {
+ export function caseSensitive(a: string, b: string) { return equals(a, b, /*ignoreCase*/ false); }
+ export function caseInsensitive(a: string, b: string) { return equals(a, b, /*ignoreCase*/ true); }
+ }
+
export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) {
if (!isAbsolute(ancestor)) throw new Error("Path not absolute");
if (!isAbsolute(descendant)) throw new Error("Path not absolute");
const ancestorComponents = reduce(parse(ancestor));
const descendantComponents = reduce(parse(descendant));
-
+ const len = Math.min(ancestorComponents.length, descendantComponents.length);
let start: number;
- for (start = 0; start < ancestorComponents.length && start < descendantComponents.length; start++) {
+ for (start = 0; start < len; start++) {
if (compareStrings(ancestorComponents[start], descendantComponents[start], ignoreCase)) {
break;
}
}
-
return start === ancestorComponents.length;
}
+ export namespace beneath {
+ export function caseSensitive(ancestor: string, descendant: string) { return beneath(ancestor, descendant, /*ignoreCase*/ false); }
+ export function caseInsensitive(ancestor: string, descendant: string) { return beneath(ancestor, descendant, /*ignoreCase*/ true); }
+ }
+
export function parse(path: string) {
- path = normalizeSlashes(path);
+ path = normalizeSeparators(path);
const rootLength = getRootLength(path);
const root = path.substring(0, rootLength);
const rest = path.substring(rootLength).split(/\/+/g);
@@ -116,19 +166,19 @@ namespace vpath {
}
export function format(components: string[]) {
- return components.length ? components[0] + components.slice(1).join("/") : "";
+ return components.length ? components[0] + components.slice(1).join(sep) : "";
}
export function dirname(path: string) {
- path = normalizeSlashes(path);
- return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf("/")));
+ path = normalizeSeparators(path);
+ return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(sep)));
}
export function basename(path: string, ext?: string): string;
export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean }): string;
export function basename(path: string, options?: { extensions?: string[], ignoreCase?: boolean } | string) {
- path = normalizeSlashes(path);
- const name = path.substr(Math.max(getRootLength(path), path.lastIndexOf("/") + 1));
+ path = normalizeSeparators(path);
+ const name = path.substr(Math.max(getRootLength(path), path.lastIndexOf(sep) + 1));
const extension = typeof options === "string" ? options.startsWith(".") ? options : "." + options :
options && options.extensions ? extname(name, options) :
undefined;
@@ -136,6 +186,7 @@ namespace vpath {
}
const extRegExp = /\.\w+$/;
+
export function extname(path: string, options?: { extensions?: string[], ignoreCase?: boolean }) {
if (options && options.extensions) {
for (let extension of options.extensions) {
@@ -153,9 +204,4 @@ namespace vpath {
const match = extRegExp.exec(path);
return match ? match[0] : "";
}
-
- export function chext(path: string, ext: string, options?: { extensions?: string[], ignoreCase?: boolean }) {
- const pathext = extname(path, options);
- return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path;
- }
}
\ No newline at end of file
diff --git a/tests/webTestServer.ts b/tests/webTestServer.ts
index 5a3b4cc5048..a1f1747adcb 100644
--- a/tests/webTestServer.ts
+++ b/tests/webTestServer.ts
@@ -4,333 +4,767 @@ 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");
-
-/// Command line processing ///
-
-if (process.argv[2] == "--help") {
- console.log("Runs a node server on port 8888, looking for tests folder in the current directory\n");
- console.log("Syntax: node nodeServer.js [typescriptEnlistmentDirectory] [tests] [--browser] [--verbose]\n");
- console.log("Examples: \n\tnode nodeServer.js .");
- console.log("\tnode nodeServer.js 3000 D:/src/typescript/public --verbose IE");
-}
-
-function switchToForwardSlashes(path: string) {
- return path.replace(/\\/g, "/").replace(/\/\//g, "/");
-}
+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: string;
-if (process.argv[2]) {
- browser = process.argv[2];
- if (browser !== "chrome" && browser !== "IE") {
- console.log(`Invalid command line arguments. Got ${browser} but expected chrome, IE or nothing.`);
+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));
}
}
-const grep = process.argv[3];
+namespace HttpHeaders {
+ export function clone(headers: http.OutgoingHttpHeaders) {
+ return { ...headers };
+ }
-let verbose = false;
-if (process.argv[4] == "--verbose") {
- verbose = true;
-}
-else if (process.argv[4] && process.argv[4] !== "--verbose") {
- console.log(`Invalid command line arguments. Got ${process.argv[4]} but expected --verbose or nothing.`);
+ 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((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 = 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 = 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" || 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: [] };
+ }
}
-/// Utils ///
function log(msg: string) {
if (verbose) {
console.log(msg);
}
}
-
-const directorySeparator = "/";
-
-function getRootLength(path: string): number {
- if (path.charAt(0) === directorySeparator) {
- if (path.charAt(1) !== directorySeparator) return 1;
- const p1 = path.indexOf("/", 2);
- if (p1 < 0) return 2;
- const p2 = path.indexOf("/", p1 + 1);
- if (p2 < 0) return p1 + 1;
- return p2 + 1;
+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";
}
- if (path.charAt(1) === ":") {
- if (path.charAt(2) === directorySeparator) return 3;
- return 2;
- }
- // Per RFC 1738 'file' URI schema has the shape file:///
- // if is omitted then it is assumed that host value is 'localhost',
- // however slash after the omitted is not removed.
- // file:///folder1/file1 - this is a correct URI
- // file://folder2/file2 - this is an incorrect URI
- if (path.lastIndexOf("file:///", 0) === 0) {
- return "file:///".length;
- }
- const idx = path.indexOf("://");
- if (idx !== -1) {
- return idx + "://".length;
- }
- return 0;
}
-function getDirectoryPath(path: string): any {
- path = switchToForwardSlashes(path);
- return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
+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(" The browser to launch. One of 'IE', 'chrome', or 'none' (default 'IE').");
+ console.log(" A regular expression to pass to Mocha.");
+ console.log(" --verbose Enables verbose logging.")
}
-function ensureDirectoriesExist(path: string) {
- path = switchToForwardSlashes(path);
- if (path.length > getRootLength(path) && !fs.existsSync(path)) {
- const parentDirectory = getDirectoryPath(path);
- ensureDirectoriesExist(parentDirectory);
- if (!fs.existsSync(path)) {
- fs.mkdirSync(path);
+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;
}
- }
-}
-
-// Copied from the compiler sources
-function dir(dirPath: string, spec?: string, options?: any) {
- options = options || <{ recursive?: boolean; }>{};
- return filesInFolder(dirPath);
-
- function filesInFolder(folder: string): string[] {
- folder = switchToForwardSlashes(folder);
- let paths: string[] = [];
- // Everything after the current directory is relative
- const baseDirectoryLength = process.cwd().length + 1;
-
- try {
- const files = fs.readdirSync(folder);
- for (let i = 0; i < files.length; i++) {
- const stat = fs.statSync(path.join(folder, files[i]));
- if (options.recursive && stat.isDirectory()) {
- paths = paths.concat(filesInFolder(path.join(folder, files[i])));
- }
- else if (stat.isFile() && (!spec || files[i].match(spec))) {
- const relativePath = folder.substring(baseDirectoryLength);
- paths.push(path.join(relativePath, files[i]));
- }
+ else if (argLower === "--verbose") {
+ verbose = true;
+ }
+ else {
+ if (offset === 0) {
+ browser = arg;
}
- }
- catch (err) {
- // Skip folders that are inaccessible
- }
- return paths;
- }
-}
-
-function writeFile(path: string, data: any) {
- ensureDirectoriesExist(getDirectoryPath(path));
- fs.writeFileSync(path, data);
-}
-
-/// Request Handling ///
-
-function handleResolutionRequest(filePath: string, res: http.ServerResponse) {
- let resolvedPath = path.resolve(filePath, "");
- resolvedPath = resolvedPath.substring(resolvedPath.indexOf("tests"));
- resolvedPath = switchToForwardSlashes(resolvedPath);
- send(ResponseCode.Success, res, resolvedPath);
-}
-
-const enum ResponseCode {
- Success = 200,
- BadRequest = 400,
- NotFound = 404,
- MethodNotAllowed = 405,
- PayloadTooLarge = 413,
- Fail = 500
-}
-
-function send(responseCode: number, res: http.ServerResponse, contents: string, contentType = "binary"): void {
- res.writeHead(responseCode, { "Content-Type": contentType });
- res.end(contents);
-}
-
-// Reads the data from a post request and passes it to the given callback
-function processPost(req: http.ServerRequest, res: http.ServerResponse, callback: (data: string) => any): void {
- let queryData = "";
- if (typeof callback !== "function") return;
-
- if (req.method == "POST") {
- req.on("data", (data: string) => {
- queryData += data;
- if (queryData.length > 1e8) {
- queryData = "";
- send(ResponseCode.PayloadTooLarge, res, undefined);
- console.log("ERROR: destroying connection");
- req.connection.destroy();
+ else if (offset === 1) {
+ grep = arg;
}
- });
+ else {
+ console.log(`Unrecognized argument: ${arg}\n`);
+ return false;
+ }
+ offset++;
+ }
+ }
- req.on("end", () => {
- // res.post = url.parse(req.url).query;
- callback(queryData);
- });
+ 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 {
- send(ResponseCode.MethodNotAllowed, res, undefined);
- }
-}
-
-enum RequestType {
- GetFile,
- GetDir,
- ResolveFile,
- WriteFile,
- DeleteFile,
- WriteDir,
- DeleteDir,
- AppendFile,
- Unknown
-}
-
-function getRequestOperation(req: http.ServerRequest) {
- if (req.method === "GET" && req.url.indexOf("?") === -1) {
- if (req.url.indexOf(".") !== -1) return RequestType.GetFile;
- else return RequestType.GetDir;
- }
- else {
-
- const queryData: any = url.parse(req.url, /*parseQueryString*/ true).query;
- if (req.method === "GET" && queryData.resolve !== undefined) return RequestType.ResolveFile;
- // mocha uses ?grep= query string as equivalent to the --grep command line option used to filter tests
- if (req.method === "GET" && queryData.grep !== undefined) return RequestType.GetFile;
- if (req.method === "POST" && queryData.action) {
- const path = req.url.substr(0, req.url.lastIndexOf("?"));
- const isFile = path.substring(path.lastIndexOf("/")).indexOf(".") !== -1;
- switch (queryData.action.toUpperCase()) {
- case "WRITE":
- return isFile ? RequestType.WriteFile : RequestType.WriteDir;
- case "DELETE":
- return isFile ? RequestType.DeleteFile : RequestType.DeleteDir;
- case "APPEND":
- return isFile ? RequestType.AppendFile : RequestType.Unknown;
- }
+ const defaultIEPath = "C:/Program Files/Internet Explorer/iexplore.exe";
+ if (fs.existsSync(defaultIEPath)) {
+ browserPath = defaultIEPath;
}
- return RequestType.Unknown;
- }
-}
-
-function handleRequestOperation(req: http.ServerRequest, res: http.ServerResponse, operation: RequestType, reqPath: string) {
- switch (operation) {
- case RequestType.GetDir:
- const filesInFolder = dir(reqPath, "", { recursive: true });
- send(ResponseCode.Success, res, filesInFolder.join(","));
- break;
- case RequestType.GetFile:
- fs.readFile(reqPath, (err, file) => {
- const contentType = contentTypeForExtension(path.extname(reqPath));
- if (err) {
- send(ResponseCode.NotFound, res, err.message, contentType);
- }
- else {
- send(ResponseCode.Success, res, file, contentType);
- }
- });
- break;
- case RequestType.ResolveFile:
- const resolveRequest = req.url.match(/(.*)\?resolve/);
- handleResolutionRequest(resolveRequest[1], res);
- break;
- case RequestType.WriteFile:
- processPost(req, res, (data) => {
- writeFile(reqPath, data);
- });
- send(ResponseCode.Success, res, undefined);
- break;
- case RequestType.WriteDir:
- fs.mkdirSync(reqPath);
- send(ResponseCode.Success, res, undefined);
- break;
- case RequestType.DeleteFile:
- if (fs.existsSync(reqPath)) {
- fs.unlinkSync(reqPath);
- }
- send(ResponseCode.Success, res, undefined);
- break;
- case RequestType.DeleteDir:
- if (fs.existsSync(reqPath)) {
- fs.rmdirSync(reqPath);
- }
- send(ResponseCode.Success, res, undefined);
- break;
- case RequestType.AppendFile:
- processPost(req, res, (data) => {
- fs.appendFileSync(reqPath, data);
- });
- send(ResponseCode.Success, res, undefined);
- break;
- case RequestType.Unknown:
- default:
- send(ResponseCode.BadRequest, res, undefined);
- break;
- }
-
- function contentTypeForExtension(ext: string) {
- switch (ext) {
- case ".js": return "text/javascript";
- case ".css": return "text/css";
- case ".html": return "text/html";
- default: return "binary";
+ 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"
+ });
}
-console.log(`Static file server running at\n => http://localhost:${port}/\nCTRL + C to shutdown`);
-
-http.createServer((req: http.ServerRequest, res: http.ServerResponse) => {
- log(`${req.method} ${req.url}`);
- const uri = decodeURIComponent(url.parse(req.url).pathname);
- const reqPath = path.join(process.cwd(), uri);
- const operation = getRequestOperation(req);
- handleRequestOperation(req, res, operation, reqPath);
-}).listen(port);
-
-let browserPath: string;
-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;
+function main() {
+ if (parseCommandLine(process.argv.slice(2))) {
+ startServer();
+ startClient();
}
}
-console.log(`Using browser: ${browserPath}`);
-
-const queryString = grep ? `?grep=${grep}` : "";
-child_process.spawn(browserPath, [`http://localhost:${port}/tests/webTestResults.html${queryString}`], {
- stdio: "inherit"
-});
+main();
\ No newline at end of file