Migrate tsserverProjectSystem to vfs

This commit is contained in:
Ron Buckton 2017-11-27 15:00:05 -08:00
parent fa428356d5
commit c5e502009f
10 changed files with 6025 additions and 5820 deletions

View File

@ -124,12 +124,16 @@ export class Timers {
* - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth.
*/
public advance(ms: number, maxDepth = 0): number {
if (ms <= 0) throw new TypeError("Argument 'ms' out of range.");
if (ms < 0) throw new TypeError("Argument 'ms' out of range.");
if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range.");
let count = 0;
const endTime = this._time + (ms | 0);
while (true) {
count += this.executeImmediates(maxDepth);
if (maxDepth >= 0) {
count += this.executeImmediates(maxDepth);
maxDepth--;
}
const dueTimer = this.dequeueIfBefore(endTime);
if (dueTimer) {
this._time = dueTimer.due;
@ -150,7 +154,7 @@ export class Timers {
* - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth.
*/
public advanceToEnd(maxDepth = 0) {
return this.remainingTime > 0 ? this.advance(this.remainingTime, maxDepth) : 0;
return this.advance(this.remainingTime, maxDepth);
}
/**

View File

@ -30,11 +30,18 @@ namespace fakes {
* Indicates whether to include a bare _lib.d.ts_.
*/
lib?: boolean;
/**
* Indicates whether to use DOS paths by default.
*/
dos?: boolean;
}
export class FakeServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost {
export class FakeServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost, ts.ModuleResolutionHost {
public static readonly dosExecutingFilePath = "c:/.ts/tsc.js";
public static readonly defaultExecutingFilePath = "/.ts/tsc.js";
public static readonly dosDefaultCurrentDirectory = "c:/";
public static readonly defaultCurrentDirectory = "/";
public static readonly dosSafeListPath = "c:/safelist.json";
public static readonly safeListPath = "/safelist.json";
public static readonly safeListContent =
`{\n` +
@ -46,6 +53,7 @@ namespace fakes {
` "chroma": "chroma-js"\n` +
`}`;
public static readonly dosLibPath = "c:/.ts/lib.d.ts";
public static readonly libPath = "/.ts/lib.d.ts";
public static readonly libContent =
`/// <reference no-default-lib="true"/>\n` +
@ -64,22 +72,32 @@ namespace fakes {
private static readonly processExitSentinel = new Error("System exit");
private readonly _output: string[] = [];
private readonly _trace: string[] = [];
private readonly _executingFilePath: string;
private readonly _getCanonicalFileName: (file: string) => string;
constructor(options: FakeServerHostOptions = {}) {
const {
dos = false,
vfs: _vfs = {},
executingFilePath = FakeServerHost.defaultExecutingFilePath,
executingFilePath = dos
? FakeServerHost.dosExecutingFilePath
: FakeServerHost.defaultExecutingFilePath,
newLine = "\n",
safeList = false,
lib = false
} = options;
const { currentDirectory = FakeServerHost.defaultCurrentDirectory, useCaseSensitiveFileNames = false } = _vfs;
const {
currentDirectory = dos
? FakeServerHost.dosDefaultCurrentDirectory
: FakeServerHost.defaultCurrentDirectory,
useCaseSensitiveFileNames = false
} = _vfs;
this.vfs = _vfs instanceof vfs.VirtualFileSystem ? _vfs :
new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames);
this.vfs = _vfs instanceof vfs.VirtualFileSystem
? _vfs
: new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames);
this.useCaseSensitiveFileNames = this.vfs.useCaseSensitiveFileNames;
this.newLine = newLine;
@ -87,11 +105,15 @@ namespace fakes {
this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames);
if (safeList) {
this.vfs.addFile(FakeServerHost.safeListPath, FakeServerHost.safeListContent);
this.vfs.addFile(
dos ? FakeServerHost.dosSafeListPath : FakeServerHost.safeListPath,
FakeServerHost.safeListContent);
}
if (lib) {
this.vfs.addFile(FakeServerHost.libPath, FakeServerHost.libContent);
this.vfs.addFile(
dos ? FakeServerHost.dosLibPath : FakeServerHost.libPath,
FakeServerHost.libContent);
}
}
@ -143,6 +165,12 @@ namespace fakes {
}
// #endregion DirectoryStructureHost members
// #region ModuleResolutionHost members
public trace(message: string) {
this._trace.push(message);
}
// #endregion
// #region System members
public readonly args: string[] = [];
@ -226,6 +254,14 @@ namespace fakes {
this._output.length = 0;
}
public getTrace(): ReadonlyArray<string> {
return this._trace;
}
public clearTrace() {
this._trace.length = 0;
}
public checkTimeoutQueueLength(expected: number) {
const callbacksCount = this.timers.getPending({ kind: "timeout", ms: this.timers.remainingTime }).length;
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
@ -236,6 +272,17 @@ namespace fakes {
this.runQueuedTimeoutCallbacks();
}
public runQueuedImmediateCallbacks() {
try {
this.timers.executeImmediates();
}
catch (e) {
if (e !== FakeServerHost.processExitSentinel) {
throw e;
}
}
}
public runQueuedTimeoutCallbacks() {
try {
this.timers.advanceToEnd();

View File

@ -6,6 +6,8 @@
namespace ts.projectSystem {
const nullCancellationToken = server.nullCancellationToken;
import libFile = ts.TestFSWithWatch.libFile;
import FileOrFolder = ts.TestFSWithWatch.FileOrFolder;
function createTestTypingsInstaller(host: server.ServerHost) {
return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host);

View File

@ -1,5 +1,6 @@
/// <reference path="..\harness.ts" />
/// <reference path="tsserverProjectSystem.ts" />
/// <reference path="../fakes.ts" />
namespace ts {
export interface Range {
@ -98,6 +99,19 @@ namespace ts {
getCurrentDirectory: notImplemented,
};
function createServerHost(files: ts.TestFSWithWatch.FileOrFolder[], options?: Partial<fakes.FakeServerHostOptions>) {
const host = new fakes.FakeServerHost(options);
for (const file of files) {
if (isString(file.content)) {
host.vfs.writeFile(file.path, file.content);
}
else {
host.vfs.addDirectory(file.path);
}
}
return host;
}
export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) {
const t = extractTest(text);
const selectionRange = t.ranges.get("selection");
@ -154,7 +168,7 @@ namespace ts {
}
function makeProgram(f: {path: string, content: string }, includeLib?: boolean) {
const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required
const host = createServerHost(includeLib ? [f, ts.TestFSWithWatch.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required
const projectService = projectSystem.createProjectService(host);
projectService.openClientFile(f.path);
const program = projectService.inferredProjects[0].getLanguageService().getProgram();
@ -178,7 +192,7 @@ namespace ts {
path: "/a.ts",
content: t.source
};
const host = projectSystem.createServerHost([f, projectSystem.libFile]);
const host = ts.TestFSWithWatch.createServerHost([f, ts.TestFSWithWatch.libFile]);
const projectService = projectSystem.createProjectService(host);
projectService.openClientFile(f.path);
const program = projectService.inferredProjects[0].getLanguageService().getProgram();

View File

@ -2,6 +2,8 @@
/// <reference path="./tsserverProjectSystem.ts" />
namespace ts.projectSystem {
import FileOrFolder = ts.TestFSWithWatch.FileOrFolder;
describe("project telemetry", () => {
it("does nothing for inferred project", () => {
const file = makeFile("/a.js");
@ -235,7 +237,7 @@ namespace ts.projectSystem {
});
});
function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder {
function makeFile(path: string, content: {} = ""): FileOrFolder {
return { path, content: isString(content) ? "" : JSON.stringify(content) };
}
}

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ namespace ts.projectSystem {
import TI = server.typingsInstaller;
import validatePackageName = JsTyping.validatePackageName;
import PackageNameValidationResult = JsTyping.PackageNameValidationResult;
import FileOrFolder = ts.TestFSWithWatch.FileOrFolder;
interface InstallerParams {
globalTypingsCacheLocation?: string;
@ -628,7 +629,7 @@ namespace ts.projectSystem {
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const p = configuredProjectAt(projectService, 0);
checkProjectActualFiles(p, [app.path, jsconfig.path]);
checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]);
ts.TestFSWithWatch.checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]);
installer.installAll(/*expectedCount*/ 1);

View File

@ -55,4 +55,52 @@ namespace utils {
}
});
}
/**
* Removes leading indentation from a template literal string.
*/
export function dedent(array: TemplateStringsArray, ...args: any[]) {
let text = array[0];
for (let i = 0; i < args.length; i++) {
text += args[i];
text += array[i + 1];
}
const lineTerminatorRegExp = /\r\n?|\n/g;
const lines: string[] = [];
const lineTerminators: string[] = [];
let match: RegExpExecArray | null;
let lineStart = 0;
while (match = lineTerminatorRegExp.exec(text)) {
lines.push(text.slice(lineStart, match.index));
lineTerminators.push(match[0]);
lineStart = match.index + match[0].length;
}
const indentation = guessIndentation(lines);
let result = "";
for (let i = 0; i < lines.length; i++) {
const lineText = lines[i];
const line = indentation ? lineText.slice(indentation) : lineText;
result += line;
result += lineTerminators[i];
}
return result;
}
function guessIndentation(lines: string[]) {
let indentation: number;
for (const line of lines) {
for (let i = 0; i < line.length && (indentation === undefined || i < indentation); i++) {
if (!ts.isWhiteSpaceLike(line.charCodeAt(i))) {
if (indentation === undefined || i < indentation) {
indentation = i;
break;
}
}
}
}
return indentation;
}
}

View File

@ -426,7 +426,8 @@ namespace vfs {
public rename(oldpath: string, newpath: string) {
oldpath = vpath.resolve(this.currentDirectory, oldpath);
newpath = vpath.resolve(this.currentDirectory, newpath);
return this.root.replaceEntry(newpath, this.getEntry(oldpath));
const oldEntry = this.getEntry(oldpath);
return oldEntry !== undefined && this.root.replaceEntry(newpath, oldEntry);
}
/**

View File

@ -274,6 +274,8 @@ interface Array<T> {}`
// temporary vfs shim
public vfs = {
watchFiles: true,
watchDirectories: true,
addFile: (path: string, content: string, options?: { overwrite?: boolean }) => {
let file = this.files.find(createFileMatcher(this, path));
if (file) {
@ -308,14 +310,31 @@ interface Array<T> {}`
this.reloadFS(this.files);
}
},
renameFile: (oldpath: string, newpath: string) => {
const oldItem = this.vfs.getFile(oldpath);
if (oldItem) {
const newIndex = this.files.findIndex(createFileMatcher(this, newpath));
if (newIndex >= 0) ts.orderedRemoveItemAt(this.files, newIndex);
oldItem.path = newpath;
this.reloadFS(this.files);
rename: (oldpath: string, newpath: string) => {
if (this.vfs.getFile(oldpath)) {
const oldIndex = this.files.findIndex(createFileMatcher(this, oldpath));
if (oldIndex >= 0) {
const oldItem = this.files[oldIndex];
ts.orderedRemoveItemAt(this.files, oldIndex);
const newIndex = this.files.findIndex(createFileMatcher(this, newpath));
if (newIndex >= 0) {
ts.orderedRemoveItemAt(this.files, newIndex);
}
this.files.push({ path: newpath, content: oldItem.content });
}
}
else {
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
if (vpath.beneath(oldpath, file.path, !this.useCaseSensitiveFileNames)) {
this.files[i] = {
path: vpath.combine(newpath, vpath.relative(oldpath, file.path, !this.useCaseSensitiveFileNames)),
content: file.content
};
}
}
}
this.reloadFS(this.files);
},
addDirectory: (path: string) => {
let file = this.files.find(createFolderMatcher(this, path));
@ -326,6 +345,13 @@ interface Array<T> {}`
}
return file;
},
removeDirectory: (path: string) => {
const index = this.files.findIndex(createFolderMatcher(this, path));
if (index >= 0) {
ts.orderedRemoveItemAt(this.files, index);
this.reloadFS(this.files);
}
}
};
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) {
@ -512,12 +538,14 @@ interface Array<T> {}`
}
else {
Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath);
// Invoke directory and recursive directory watcher for the folder
// Here we arent invoking recursive directory watchers for the base folders
// since that is something we would want to do for both file as well as folder we are deleting
invokeWatcherCallbacks(this.watchedDirectories.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath));
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath));
if (this.vfs.watchDirectories) {
const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath);
// Invoke directory and recursive directory watcher for the folder
// Here we arent invoking recursive directory watchers for the base folders
// since that is something we would want to do for both file as well as folder we are deleting
invokeWatcherCallbacks(this.watchedDirectories.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath));
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath));
}
}
if (basePath !== fileOrDirectory.path) {
@ -531,6 +559,7 @@ interface Array<T> {}`
}
private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) {
if (!this.vfs.watchFiles) return;
const callbacks = this.watchedFiles.get(this.toPath(fileFullPath));
invokeWatcherCallbacks(callbacks, ({ cb, fileName }) => cb(fileName, eventKind));
}
@ -543,6 +572,7 @@ interface Array<T> {}`
* This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders
*/
private invokeDirectoryWatcher(folderFullPath: string, fileName: string) {
if (!this.vfs.watchDirectories) return;
const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath));
this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName);
@ -556,6 +586,7 @@ interface Array<T> {}`
* This will call the recursive directory watcher for this directory as well as all the base directories
*/
private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) {
if (!this.vfs.watchDirectories) return;
const relativePath = this.getRelativePathToDirectory(fullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => this.directoryCallback(cb, relativePath));
const basePath = getDirectoryPath(fullPath);