This commit is contained in:
Ryan Cavanaugh
2017-11-16 15:01:26 -08:00
parent 9d5b07e445
commit c964f220e7
7 changed files with 868 additions and 77 deletions
+11
View File
@@ -2314,6 +2314,17 @@ namespace ts {
return <T>(removeFileExtension(path) + newExtension);
}
/**
* Takes a string like "jquery-min.4.2.3" and returns "jquery"
*/
export function removeMinAndVersionNumbers(fileName: string) {
// Match a "." or "-" followed by a version number or 'min' at the end of the name
const trailingMinOrVersion = /[.-]((min)|(\d+(\.\d+)*))$/;
// The "min" or version may both be present, in either order, so try applying the above twice.
return fileName.replace(trailingMinOrVersion, "").replace(trailingMinOrVersion, "");
}
export interface ObjectAllocator {
getNodeConstructor(): new (kind: SyntaxKind, pos?: number, end?: number) => Node;
getTokenConstructor(): new <TKind extends SyntaxKind>(kind: TKind, pos?: number, end?: number) => Token<TKind>;
@@ -1484,7 +1484,7 @@ namespace ts.projectSystem {
it("ignores files excluded by a custom safe type list", () => {
const file1 = {
path: "/a/b/f1.ts",
path: "/a/b/f1.js",
content: "export let x = 5"
};
const office = {
@@ -1506,7 +1506,7 @@ namespace ts.projectSystem {
it("ignores files excluded by the default type list", () => {
const file1 = {
path: "/a/b/f1.ts",
path: "/a/b/f1.js",
content: "export let x = 5"
};
const minFile = {
+79 -37
View File
@@ -301,35 +301,35 @@ namespace ts.projectSystem {
// 1. react typings are installed for .jsx
// 2. loose files names are matched against safe list for typings if
// this is a JS project (only js, jsx, d.ts files are present)
const file1 = {
const lodashJs = {
path: "/a/b/lodash.js",
content: ""
};
const file2 = {
const file2Jsx = {
path: "/a/b/file2.jsx",
content: ""
};
const file3 = {
const file3dts = {
path: "/a/b/file3.d.ts",
content: ""
};
const react = {
const reactDts = {
path: "/a/data/node_modules/@types/react/index.d.ts",
content: "declare const react: { x: number }"
};
const lodash = {
const lodashDts = {
path: "/a/data/node_modules/@types/lodash/index.d.ts",
content: "declare const lodash: { x: number }"
};
const host = createServerHost([file1, file2, file3]);
const host = createServerHost([lodashJs, file2Jsx, file3dts]);
const installer = new (class extends Installer {
constructor() {
super(host, { typesRegistry: createTypesRegistry("lodash", "react") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
const installedTypings = ["@types/lodash", "@types/react"];
const typingFiles = [lodash, react];
const typingFiles = [lodashDts, reactDts];
executeCommand(this, host, installedTypings, typingFiles, cb);
}
})();
@@ -339,40 +339,37 @@ namespace ts.projectSystem {
projectService.openExternalProject({
projectFileName,
options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs },
rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)],
typeAcquisition: {}
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(file2Jsx.path), toExternalFile(file3dts.path)],
typeAcquisition: { }
});
const p = projectService.externalProjects[0];
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(p, [file1.path, file2.path, file3.path]);
checkProjectActualFiles(p, [file2Jsx.path, file3dts.path]);
installer.installAll(/*expectedCount*/ 1);
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]);
host.checkTimeoutQueueLength(2);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectActualFiles(p, [file2Jsx.path, file3dts.path, lodashDts.path, reactDts.path]);
});
it("external project - no type acquisition, with js & ts files", () => {
it("external project - type acquisition with enable: false", () => {
// Tests:
// 1. No typings are included for JS projects when the project contains ts files
const file1 = {
// Exclude
const jqueryJs = {
path: "/a/b/jquery.js",
content: ""
};
const file2 = {
path: "/a/b/file2.ts",
content: ""
};
const host = createServerHost([file1, file2]);
let enqueueIsCalled = false;
const host = createServerHost([jqueryJs]);
const installer = new (class extends Installer {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery") });
}
enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: server.SortedReadonlyArray<string>) {
enqueueIsCalled = true;
super.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
@@ -387,18 +384,62 @@ namespace ts.projectSystem {
projectService.openExternalProject({
projectFileName,
options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs },
rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path)],
rootFiles: [toExternalFile(jqueryJs.path)],
typeAcquisition: { enable: false }
});
const p = projectService.externalProjects[0];
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(p, [jqueryJs.path]);
installer.checkPendingCommands(/*expectedCount*/ 0);
});
it("external project - no type acquisition, with js & ts files", () => {
// Tests:
// 1. No typings are included for JS projects when the project contains ts files
const jqueryJs = {
path: "/a/b/jquery.js",
content: ""
};
const file2Ts = {
path: "/a/b/file2.ts",
content: ""
};
const host = createServerHost([jqueryJs, file2Ts]);
const installer = new (class extends Installer {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery") });
}
enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: server.SortedReadonlyArray<string>) {
super.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
const installedTypings: string[] = [];
const typingFiles: FileOrFolder[] = [];
executeCommand(this, host, installedTypings, typingFiles, cb);
}
})();
const projectFileName = "/a/app/test.csproj";
const projectService = createProjectService(host, { typingsInstaller: installer });
projectService.openExternalProject({
projectFileName,
options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs },
rootFiles: [toExternalFile(jqueryJs.path), toExternalFile(file2Ts.path)],
typeAcquisition: {}
});
const p = projectService.externalProjects[0];
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(p, [file2.path]);
checkProjectActualFiles(p, [jqueryJs.path, file2Ts.path]);
installer.checkPendingCommands(/*expectedCount*/ 0);
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectActualFiles(p, [file2.path]);
checkProjectActualFiles(p, [jqueryJs.path, file2Ts.path]);
});
it("external project - with type acquisition, with only js, d.ts files", () => {
@@ -406,15 +447,15 @@ namespace ts.projectSystem {
// 1. Safelist matching, type acquisition includes/excludes and package.json typings are all acquired
// 2. Types for safelist matches are not included when they also appear in the type acquisition exclude list
// 3. Multiple includes and excludes are respected in type acquisition
const file1 = {
const lodashJs = {
path: "/a/b/lodash.js",
content: ""
};
const file2 = {
const commanderJs = {
path: "/a/b/commander.js",
content: ""
};
const file3 = {
const file3dts = {
path: "/a/b/file3.d.ts",
content: ""
};
@@ -445,7 +486,7 @@ namespace ts.projectSystem {
content: "declare const moment: { x: number }"
};
const host = createServerHost([file1, file2, file3, packageJson]);
const host = createServerHost([lodashJs, commanderJs, file3dts, packageJson]);
const installer = new (class extends Installer {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery", "commander", "moment", "express") });
@@ -462,13 +503,13 @@ namespace ts.projectSystem {
projectService.openExternalProject({
projectFileName,
options: { allowJS: true, moduleResolution: ModuleResolutionKind.NodeJs },
rootFiles: [toExternalFile(file1.path), toExternalFile(file2.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "moment"], exclude: ["lodash"] }
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3dts.path)],
typeAcquisition: { enable: true, include: ["jquery", "moment"], exclude: ["lodash"] }
});
const p = projectService.externalProjects[0];
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(p, [file1.path, file2.path, file3.path]);
checkProjectActualFiles(p, [file3dts.path]);
installer.installAll(/*expectedCount*/ 1);
@@ -481,7 +522,7 @@ namespace ts.projectSystem {
// Moment: Specified in 'include'
// Express: Specified in package.json
// lodash: Excluded (not present)
checkProjectActualFiles(p, [file3.path, commander.path, express.path, jquery.path, moment.path]);
checkProjectActualFiles(p, [file3dts.path, commander.path, express.path, jquery.path, moment.path]);
});
it("Throttle - delayed typings to install", () => {
@@ -551,7 +592,7 @@ namespace ts.projectSystem {
const p = projectService.externalProjects[0];
projectService.checkNumberOfProjects({ externalProjects: 1 });
checkProjectActualFiles(p, [lodashJs.path, commanderJs.path, file3.path]);
checkProjectActualFiles(p, [file3.path]);
installer.checkPendingCommands(/*expectedCount*/ 1);
installer.executePendingCommands();
// expected all typings file to exist
@@ -560,7 +601,7 @@ namespace ts.projectSystem {
}
checkNumberOfProjects(projectService, { externalProjects: 1 });
checkProjectActualFiles(p, [file3.path, commander.path, express.path, jquery.path, moment.path, lodash.path]);
checkProjectActualFiles(p, [file3.path, commander.path, jquery.path, moment.path, lodash.path]);
});
it("Throttle - delayed run install requests", () => {
@@ -651,7 +692,7 @@ namespace ts.projectSystem {
const p1 = projectService.externalProjects[0];
const p2 = projectService.externalProjects[1];
projectService.checkNumberOfProjects({ externalProjects: 2 });
checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path]);
checkProjectActualFiles(p1, [file3.path]);
checkProjectActualFiles(p2, [file3.path]);
installer.executePendingCommands();
@@ -661,8 +702,9 @@ namespace ts.projectSystem {
assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests");
installer.executePendingCommands();
checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path, commander.path, jquery.path, lodash.path, cordova.path]);
host.checkTimeoutQueueLength(3); // for 2 projects and 1 refreshing inferred project
host.runQueuedTimeoutCallbacks();
checkProjectActualFiles(p1, [file3.path, commander.path, jquery.path, lodash.path, cordova.path]);
checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]);
});
+680
View File
@@ -0,0 +1,680 @@
/// <reference path="harness.ts" />
namespace ts.TestFSWithWatch {
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
export const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
export const safeList = {
path: <Path>"/safeList.json",
content: JSON.stringify({
commander: "commander",
express: "express",
jquery: "jquery",
lodash: "lodash",
moment: "moment",
chroma: "chroma-js"
})
};
function getExecutingFilePathFromLibFile(): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
interface TestServerHostCreationParameters {
useCaseSensitiveFileNames?: boolean;
executingFilePath?: string;
currentDirectory?: string;
newLine?: string;
useWindowsStylePaths?: boolean;
}
export function createWatchedSystem(fileOrFolderList: ReadonlyArray<FileOrFolder>, params?: TestServerHostCreationParameters): TestServerHost {
if (!params) {
params = {};
}
const host = new TestServerHost(/*withSafelist*/ false,
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
params.executingFilePath || getExecutingFilePathFromLibFile(),
params.currentDirectory || "/",
fileOrFolderList,
params.newLine,
params.useWindowsStylePaths);
return host;
}
export function createServerHost(fileOrFolderList: ReadonlyArray<FileOrFolder>, params?: TestServerHostCreationParameters): TestServerHost {
if (!params) {
params = {};
}
const host = new TestServerHost(/*withSafelist*/ true,
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
params.executingFilePath || getExecutingFilePathFromLibFile(),
params.currentDirectory || "/",
fileOrFolderList,
params.newLine,
params.useWindowsStylePaths);
return host;
}
export interface FileOrFolder {
path: string;
content?: string;
fileSize?: number;
}
interface FSEntry {
path: Path;
fullPath: string;
}
interface File extends FSEntry {
content: string;
fileSize?: number;
}
interface Folder extends FSEntry {
entries: FSEntry[];
}
function isFolder(s: FSEntry): s is Folder {
return s && isArray((<Folder>s).entries);
}
function isFile(s: FSEntry): s is File {
return s && isString((<File>s).content);
}
function invokeWatcherCallbacks<T>(callbacks: T[], invokeCallback: (cb: T) => void): void {
if (callbacks) {
// The array copy is made to ensure that even if one of the callback removes the callbacks,
// we dont miss any callbacks following it
const cbs = callbacks.slice();
for (const cb of cbs) {
invokeCallback(cb);
}
}
}
function getDiffInKeys<T>(map: Map<T>, expectedKeys: ReadonlyArray<string>) {
if (map.size === expectedKeys.length) {
return "";
}
const notInActual: string[] = [];
const duplicates: string[] = [];
const seen = createMap<true>();
forEach(expectedKeys, expectedKey => {
if (seen.has(expectedKey)) {
duplicates.push(expectedKey);
return;
}
seen.set(expectedKey, true);
if (!map.has(expectedKey)) {
notInActual.push(expectedKey);
}
});
const inActualNotExpected: string[] = [];
map.forEach((_value, key) => {
if (!seen.has(key)) {
inActualNotExpected.push(key);
}
seen.set(key, true);
});
return `\n\nNotInActual: ${notInActual}\nDuplicates: ${duplicates}\nInActualButNotInExpected: ${inActualNotExpected}`;
}
export function verifyMapSize(caption: string, map: Map<any>, expectedKeys: ReadonlyArray<string>) {
assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`);
}
function checkMapKeys(caption: string, map: Map<any>, expectedKeys: ReadonlyArray<string>) {
verifyMapSize(caption, map, expectedKeys);
for (const name of expectedKeys) {
assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`);
}
}
export function checkFileNames(caption: string, actualFileNames: ReadonlyArray<string>, expectedFileNames: string[]) {
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected:\r\n${expectedFileNames.join("\r\n")}\r\ngot: ${actualFileNames.join("\r\n")}`);
for (const f of expectedFileNames) {
assert.equal(true, contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`);
}
}
export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
}
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
}
export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {
const mapExpected = arrayToSet(expected);
const mapSeen = createMap<true>();
for (const f of host.getOutput()) {
assert.isUndefined(mapSeen.get(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`);
if (mapExpected.has(f)) {
mapExpected.delete(f);
mapSeen.set(f, true);
}
}
assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`);
}
export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray<string>) {
const mapExpectedToBeAbsent = arrayToSet(expectedToBeAbsent);
for (const f of host.getOutput()) {
assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`);
}
}
class Callbacks {
private map: TimeOutCallback[] = [];
private nextId = 1;
getNextId() {
return this.nextId;
}
register(cb: (...args: any[]) => void, args: any[]) {
const timeoutId = this.nextId;
this.nextId++;
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
return timeoutId;
}
unregister(id: any) {
if (typeof id === "number") {
delete this.map[id];
}
}
count() {
let n = 0;
for (const _ in this.map) {
n++;
}
return n;
}
invoke(invokeKey?: number) {
if (invokeKey) {
this.map[invokeKey]();
delete this.map[invokeKey];
return;
}
// Note: invoking a callback may result in new callbacks been queued,
// so do not clear the entire callback list regardless. Only remove the
// ones we have invoked.
for (const key in this.map) {
this.map[key]();
delete this.map[key];
}
}
}
type TimeOutCallback = () => any;
export interface TestFileWatcher {
cb: FileWatcherCallback;
fileName: string;
}
export interface TestDirectoryWatcher {
cb: DirectoryWatcherCallback;
directoryName: string;
}
export interface ReloadWatchInvokeOptions {
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
}
export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost {
args: string[] = [];
private readonly output: string[] = [];
private fs: Map<FSEntry> = createMap<FSEntry>();
getCanonicalFileName: (s: string) => string;
private toPath: (f: string) => Path;
private timeoutCallbacks = new Callbacks();
private immediateCallbacks = new Callbacks();
readonly watchedDirectories = createMultiMap<TestDirectoryWatcher>();
readonly watchedDirectoriesRecursive = createMultiMap<TestDirectoryWatcher>();
readonly watchedFiles = createMultiMap<TestFileWatcher>();
private readonly executingFilePath: string;
private readonly currentDirectory: string;
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) {
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
this.reloadFS(fileOrFolderList);
}
getNewLine() {
return this.newLine;
}
toNormalizedAbsolutePath(s: string) {
return getNormalizedAbsolutePath(s, this.currentDirectory);
}
toFullPath(s: string) {
return this.toPath(this.toNormalizedAbsolutePath(s));
}
getHostSpecificPath(s: string) {
if (this.useWindowsStylePath && s.startsWith(directorySeparator)) {
return "c:/" + s.substring(1);
}
return s;
}
reloadFS(fileOrFolderList: ReadonlyArray<FileOrFolder>, options?: Partial<ReloadWatchInvokeOptions>) {
const mapNewLeaves = createMap<true>();
const isNewFs = this.fs.size === 0;
fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []);
const filesOrFoldersToLoad: ReadonlyArray<FileOrFolder> = !this.useWindowsStylePath ? fileOrFolderList :
fileOrFolderList.map<FileOrFolder>(f => {
const result = clone(f);
result.path = this.getHostSpecificPath(f.path);
return result;
});
for (const fileOrDirectory of filesOrFoldersToLoad) {
const path = this.toFullPath(fileOrDirectory.path);
mapNewLeaves.set(path, true);
// If its a change
const currentEntry = this.fs.get(path);
if (currentEntry) {
if (isFile(currentEntry)) {
if (isString(fileOrDirectory.content)) {
// Update file
if (currentEntry.content !== fileOrDirectory.content) {
currentEntry.content = fileOrDirectory.content;
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath);
}
else {
this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed);
}
}
}
else {
// TODO: Changing from file => folder
}
}
else {
// Folder
if (isString(fileOrDirectory.content)) {
// TODO: Changing from folder => file
}
else {
// Folder update: Nothing to do.
}
}
}
else {
this.ensureFileOrFolder(fileOrDirectory, options && options.ignoreWatchInvokedWithTriggerAsFileCreate);
}
}
if (!isNewFs) {
this.fs.forEach((fileOrDirectory, path) => {
// If this entry is not from the new file or folder
if (!mapNewLeaves.get(path)) {
// Leaf entries that arent in new list => remove these
if (isFile(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) {
this.removeFileOrFolder(fileOrDirectory, folder => !mapNewLeaves.get(folder.path));
}
}
});
}
}
renameFolder(folderName: string, newFolderName: string) {
const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory);
const path = this.toPath(fullPath);
const folder = this.fs.get(path) as Folder;
Debug.assert(!!folder);
// Only remove the folder
this.removeFileOrFolder(folder, returnFalse, /*isRenaming*/ true);
// Add updated folder with new folder name
const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory);
const newFolder = this.toFolder(newFullPath);
const newPath = newFolder.path;
const basePath = getDirectoryPath(path);
Debug.assert(basePath !== path);
Debug.assert(basePath === getDirectoryPath(newPath));
const baseFolder = this.fs.get(basePath) as Folder;
this.addFileOrFolderInFolder(baseFolder, newFolder);
// Invoke watches for files in the folder as deleted (from old path)
for (const entry of folder.entries) {
Debug.assert(isFile(entry));
this.fs.delete(entry.path);
this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Deleted);
entry.fullPath = combinePaths(newFullPath, getBaseFileName(entry.fullPath));
entry.path = this.toPath(entry.fullPath);
newFolder.entries.push(entry);
this.fs.set(entry.path, entry);
this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Created);
}
}
ensureFileOrFolder(fileOrDirectory: FileOrFolder, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean) {
if (isString(fileOrDirectory.content)) {
const file = this.toFile(fileOrDirectory);
Debug.assert(!this.fs.get(file.path));
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
}
else {
const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory);
this.ensureFolder(fullPath);
}
}
private ensureFolder(fullPath: string): Folder {
const path = this.toPath(fullPath);
let folder = this.fs.get(path) as Folder;
if (!folder) {
folder = this.toFolder(fullPath);
const baseFullPath = getDirectoryPath(fullPath);
if (fullPath !== baseFullPath) {
// Add folder in the base folder
const baseFolder = this.ensureFolder(baseFullPath);
this.addFileOrFolderInFolder(baseFolder, folder);
}
else {
// root folder
Debug.assert(this.fs.size === 0);
this.fs.set(path, folder);
}
}
Debug.assert(isFolder(folder));
return folder;
}
private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder, ignoreWatch?: boolean) {
folder.entries.push(fileOrDirectory);
this.fs.set(fileOrDirectory.path, fileOrDirectory);
if (ignoreWatch) {
return;
}
if (isFile(fileOrDirectory)) {
this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created);
}
this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath);
}
private removeFileOrFolder(fileOrDirectory: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) {
const basePath = getDirectoryPath(fileOrDirectory.path);
const baseFolder = this.fs.get(basePath) as Folder;
if (basePath !== fileOrDirectory.path) {
Debug.assert(!!baseFolder);
filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory);
}
this.fs.delete(fileOrDirectory.path);
if (isFile(fileOrDirectory)) {
this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted);
}
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 (basePath !== fileOrDirectory.path) {
if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) {
this.removeFileOrFolder(baseFolder, isRemovableLeafFolder);
}
else {
this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrDirectory.fullPath);
}
}
}
private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) {
const callbacks = this.watchedFiles.get(this.toPath(fileFullPath));
invokeWatcherCallbacks(callbacks, ({ cb, fileName }) => cb(fileName, eventKind));
}
private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
}
/**
* This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders
*/
private invokeDirectoryWatcher(folderFullPath: string, fileName: string) {
const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath));
this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName);
}
private directoryCallback({ cb, directoryName }: TestDirectoryWatcher, relativePath: string) {
cb(combinePaths(directoryName, relativePath));
}
/**
* This will call the recursive directory watcher for this directory as well as all the base directories
*/
private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) {
const relativePath = this.getRelativePathToDirectory(fullPath, fileName);
invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => this.directoryCallback(cb, relativePath));
const basePath = getDirectoryPath(fullPath);
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
this.invokeRecursiveDirectoryWatcher(basePath, fileName);
}
}
private toFile(fileOrDirectory: FileOrFolder): File {
const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory);
return {
path: this.toPath(fullPath),
content: fileOrDirectory.content,
fullPath,
fileSize: fileOrDirectory.fileSize
};
}
private toFolder(path: string): Folder {
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
return {
path: this.toPath(fullPath),
entries: [],
fullPath
};
}
fileExists(s: string) {
const path = this.toFullPath(s);
return isFile(this.fs.get(path));
}
readFile(s: string) {
const fsEntry = this.fs.get(this.toFullPath(s));
return isFile(fsEntry) ? fsEntry.content : undefined;
}
getFileSize(s: string) {
const path = this.toFullPath(s);
const entry = this.fs.get(path);
if (isFile(entry)) {
return entry.fileSize ? entry.fileSize : entry.content.length;
}
return undefined;
}
directoryExists(s: string) {
const path = this.toFullPath(s);
return isFolder(this.fs.get(path));
}
getDirectories(s: string) {
const path = this.toFullPath(s);
const folder = this.fs.get(path);
if (isFolder(folder)) {
return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
}
Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
return [];
}
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[] {
return ts.matchFiles(this.toNormalizedAbsolutePath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
const directories: string[] = [];
const files: string[] = [];
const dirEntry = this.fs.get(this.toPath(dir));
if (isFolder(dirEntry)) {
dirEntry.entries.forEach((entry) => {
if (isFolder(entry)) {
directories.push(getBaseFileName(entry.fullPath));
}
else if (isFile(entry)) {
files.push(getBaseFileName(entry.fullPath));
}
else {
Debug.fail("Unknown entry");
}
});
}
return { directories, files };
});
}
watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher {
const path = this.toFullPath(directoryName);
const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories;
const callback: TestDirectoryWatcher = {
cb,
directoryName
};
map.add(path, callback);
return {
close: () => map.remove(path, callback)
};
}
createHash(s: string): string {
return Harness.mockHash(s);
}
watchFile(fileName: string, cb: FileWatcherCallback) {
const path = this.toFullPath(fileName);
const callback: TestFileWatcher = { fileName, cb };
this.watchedFiles.add(path, callback);
return { close: () => this.watchedFiles.remove(path, callback) };
}
// TOOD: record and invoke callbacks to simulate timer events
setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) {
return this.timeoutCallbacks.register(callback, args);
}
getNextTimeoutId() {
return this.timeoutCallbacks.getNextId();
}
clearTimeout(timeoutId: any): void {
this.timeoutCallbacks.unregister(timeoutId);
}
checkTimeoutQueueLengthAndRun(expected: number) {
this.checkTimeoutQueueLength(expected);
this.runQueuedTimeoutCallbacks();
}
checkTimeoutQueueLength(expected: number) {
const callbacksCount = this.timeoutCallbacks.count();
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
}
runQueuedTimeoutCallbacks(timeoutId?: number) {
try {
this.timeoutCallbacks.invoke(timeoutId);
}
catch (e) {
if (e.message === this.existMessage) {
return;
}
throw e;
}
}
runQueuedImmediateCallbacks() {
this.immediateCallbacks.invoke();
}
setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) {
return this.immediateCallbacks.register(callback, args);
}
clearImmediate(timeoutId: any): void {
this.immediateCallbacks.unregister(timeoutId);
}
createDirectory(directoryName: string): void {
const folder = this.toFolder(directoryName);
// base folder has to be present
const base = getDirectoryPath(folder.path);
const baseFolder = this.fs.get(base) as Folder;
Debug.assert(isFolder(baseFolder));
Debug.assert(!this.fs.get(folder.path));
this.addFileOrFolderInFolder(baseFolder, folder);
}
writeFile(path: string, content: string): void {
const file = this.toFile({ path, content });
// base folder has to be present
const base = getDirectoryPath(file.path);
const folder = this.fs.get(base) as Folder;
Debug.assert(isFolder(folder));
this.addFileOrFolderInFolder(folder, file);
}
write(message: string) {
this.output.push(message);
}
getOutput(): ReadonlyArray<string> {
return this.output;
}
clearOutput() {
clear(this.output);
}
readonly existMessage = "System Exit";
exitCode: number;
readonly resolvePath = (s: string) => s;
readonly getExecutingFilePath = () => this.executingFilePath;
readonly getCurrentDirectory = () => this.currentDirectory;
exit(exitCode?: number) {
this.exitCode = exitCode;
throw new Error(this.existMessage);
}
readonly getEnvironmentVariable = notImplemented;
}
}
+86 -15
View File
@@ -109,6 +109,14 @@ namespace ts.server {
"smart": IndentStyle.Smart
});
<<<<<<< HEAD
=======
export interface TypesMapFile {
typesMap: SafeList;
simpleMap: { [libName: string]: string };
}
>>>>>>> f2931a1320... Port PR #20048
/**
* How to understand this block:
* * The 'match' property is a regexp that matches a filename.
@@ -387,6 +395,7 @@ namespace ts.server {
private readonly hostConfiguration: HostConfiguration;
private safelist: SafeList = defaultTypeSafeList;
private legacySafelist: { [key: string]: string } = {};
private changedFiles: ScriptInfo[];
@@ -472,6 +481,32 @@ namespace ts.server {
this.eventHandler(event);
}
<<<<<<< HEAD
=======
private loadTypesMap() {
try {
const fileContent = this.host.readFile(this.typesMapLocation);
if (fileContent === undefined) {
this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`);
return;
}
const raw: TypesMapFile = JSON.parse(fileContent);
// Parse the regexps
for (const k of Object.keys(raw.typesMap)) {
raw.typesMap[k].match = new RegExp(raw.typesMap[k].match as {} as string, "i");
}
// raw is now fixed and ready
this.safelist = raw.typesMap;
this.legacySafelist = raw.simpleMap;
}
catch (e) {
this.logger.info(`Error loading types map: ${e}`);
this.safelist = defaultTypeSafeList;
this.legacySafelist = {};
}
}
>>>>>>> f2931a1320... Port PR #20048
updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void {
const project = this.findProject(response.projectName);
if (!project) {
@@ -1744,11 +1779,17 @@ namespace ts.server {
this.safelist = raw;
}
applySafeList(proj: protocol.ExternalProject): void {
applySafeList(proj: protocol.ExternalProject): NormalizedPath[] {
const { rootFiles, typeAcquisition } = proj;
const types = (typeAcquisition && typeAcquisition.include) || [];
Debug.assert(!!typeAcquisition, "proj.typeAcquisition should be set by now");
// If type acquisition has been explicitly disabled, do not exclude anything from the project
if (typeAcquisition.enable === false) {
return [];
}
const typeAcqInclude = typeAcquisition.include || (typeAcquisition.include = []);
const excludeRules: string[] = [];
const excludedFiles: NormalizedPath[] = [];
const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName));
@@ -1761,8 +1802,10 @@ namespace ts.server {
// If the file matches, collect its types packages and exclude rules
if (rule.types) {
for (const type of rule.types) {
if (types.indexOf(type) < 0) {
types.push(type);
// Best-effort de-duping here - doesn't need to be unduplicated but
// we don't want the list to become a 400-element array of just 'kendo'
if (typeAcqInclude.indexOf(type) < 0) {
typeAcqInclude.push(type);
}
}
}
@@ -1800,16 +1843,42 @@ namespace ts.server {
}
}
}
// Copy back this field into the project if needed
if (types.length > 0) {
proj.typeAcquisition = proj.typeAcquisition || {};
proj.typeAcquisition.include = types;
}
}
const excludeRegexes = excludeRules.map(e => new RegExp(e, "i"));
proj.rootFiles = proj.rootFiles.filter((_file, index) => !excludeRegexes.some(re => re.test(normalizedNames[index])));
const filesToKeep: ts.server.protocol.ExternalFile[] = [];
for (let i = 0; i < proj.rootFiles.length; i++) {
if (excludeRegexes.some(re => re.test(normalizedNames[i]))) {
excludedFiles.push(normalizedNames[i]);
}
else {
let exclude = false;
if (typeAcquisition.enable || typeAcquisition.enableAutoDiscovery) {
const baseName = getBaseFileName(normalizedNames[i].toLowerCase());
if (fileExtensionIs(baseName, "js")) {
const inferredTypingName = removeFileExtension(baseName);
const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName);
if (this.legacySafelist[cleanedTypingName]) {
this.logger.info(`Excluded '${normalizedNames[i]}' because it matched ${cleanedTypingName} from the legacy safelist`);
excludedFiles.push(normalizedNames[i]);
// *exclude* it from the project...
exclude = true;
// ... but *include* it in the list of types to acquire
const typeName = this.legacySafelist[cleanedTypingName];
// Same best-effort dedupe as above
if (typeAcqInclude.indexOf(typeName) < 0) {
typeAcqInclude.push(typeName);
}
}
}
}
if (!exclude) {
filesToKeep.push(proj.rootFiles[i]);
}
}
}
proj.rootFiles = filesToKeep;
return excludedFiles;
}
openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void {
@@ -1819,6 +1888,12 @@ namespace ts.server {
const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions);
proj.typeAcquisition = typeAcquisition;
}
proj.typeAcquisition = proj.typeAcquisition || {};
proj.typeAcquisition.include = proj.typeAcquisition.include || [];
proj.typeAcquisition.exclude = proj.typeAcquisition.exclude || [];
if (proj.typeAcquisition.enable === undefined) {
proj.typeAcquisition.enable = hasNoTypeScriptSource(proj.rootFiles.map(f => f.fileName));
}
this.applySafeList(proj);
@@ -1913,11 +1988,7 @@ namespace ts.server {
else {
// no config files - remove the item from the collection
this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName);
<<<<<<< HEAD
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition);
=======
this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles);
>>>>>>> b9a548cd13... Squash port of PR #19542
}
if (!suppressRefreshOfInferredProjects) {
this.refreshInferredProjects();
+9 -20
View File
@@ -52,6 +52,11 @@ namespace ts.server {
return counts.ts === 0 && counts.tsx === 0;
}
/* @internal */
export function hasNoTypeScriptSource(fileNames: string[]): boolean {
return !fileNames.some(fileName => (fileExtensionIs(fileName, Extension.Ts) && !fileExtensionIs(fileName, Extension.Dts)) || fileExtensionIs(fileName, Extension.Tsx));
}
/* @internal */
export interface ProjectFilesWithTSDiagnostics extends protocol.ProjectFiles {
projectErrors: ReadonlyArray<Diagnostic>;
@@ -1216,26 +1221,10 @@ namespace ts.server {
}
setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void {
if (!newTypeAcquisition) {
// set default typings options
newTypeAcquisition = {
enable: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
else {
if (newTypeAcquisition.enable === undefined) {
// if autoDiscovery was not specified by the caller - set it based on the content of the project
newTypeAcquisition.enable = allRootFilesAreJsOrDts(this);
}
if (!newTypeAcquisition.include) {
newTypeAcquisition.include = [];
}
if (!newTypeAcquisition.exclude) {
newTypeAcquisition.exclude = [];
}
}
Debug.assert(!!newTypeAcquisition, "newTypeAcquisition may not be null/undefined");
Debug.assert(!!newTypeAcquisition.include, "newTypeAcquisition.include may not be null/undefined");
Debug.assert(!!newTypeAcquisition.exclude, "newTypeAcquisition.exclude may not be null/undefined");
Debug.assert(typeof newTypeAcquisition.enable === "boolean", "newTypeAcquisition.enable may not be null/undefined");
this.typeAcquisition = newTypeAcquisition;
}
}
@@ -161,9 +161,6 @@ namespace ts.server.typingsInstaller {
this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`);
}
// respond with whatever cached typings we have now
this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths));
// start watching files
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
@@ -172,6 +169,7 @@ namespace ts.server.typingsInstaller {
this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames);
}
else {
this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths));
if (this.log.isEnabled()) {
this.log.writeLine(`No new typings were requested as a result of typings discovery`);
}