diff --git a/Gulpfile.ts b/Gulpfile.ts index 63bdb52e435..a92ace9644f 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -463,10 +463,11 @@ gulp.task(serverFile, /*help*/ false, [servicesFile, typingsInstallerJs, cancell .pipe(gulp.dest("src/server")); }); +const typesMapJson = path.join(builtLocalDirectory, "typesMap.json"); const tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js"); const tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts"); -gulp.task(tsserverLibraryFile, /*help*/ false, [servicesFile], (done) => { +gulp.task(tsserverLibraryFile, /*help*/ false, [servicesFile, typesMapJson], (done) => { const serverLibraryProject = tsc.createProject("src/server/tsconfig.library.json", getCompilerSettings({}, /*useBuiltCompiler*/ true)); const {js, dts}: { js: NodeJS.ReadableStream, dts: NodeJS.ReadableStream } = serverLibraryProject.src() .pipe(sourcemaps.init()) @@ -485,6 +486,15 @@ gulp.task(tsserverLibraryFile, /*help*/ false, [servicesFile], (done) => { ]); }); +gulp.task(typesMapJson, /*help*/ false, [], () => { + return gulp.src("src/server/typesMap.json") + .pipe(insert.transform((contents, file) => { + JSON.parse(contents); + return contents; + })) + .pipe(gulp.dest(builtLocalDirectory)); +}); + gulp.task("lssl", "Builds language service server library", [tsserverLibraryFile]); gulp.task("local", "Builds the full compiler and services", [builtLocalCompiler, servicesFile, serverFile, builtGeneratedDiagnosticMessagesJSON, tsserverLibraryFile]); gulp.task("tsc", "Builds only the compiler", [builtLocalCompiler]); diff --git a/Jakefile.js b/Jakefile.js index 15fe40141f6..20d803f42ca 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -88,6 +88,8 @@ var watchGuardSources = filesFromConfig(path.join(serverDirectory, "watchGuard/t var serverSources = filesFromConfig(path.join(serverDirectory, "tsconfig.json")) var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, "tsconfig.library.json")); +var typesMapOutputPath = path.join(builtLocalDirectory, 'typesMap.json'); + var harnessCoreSources = [ "harness.ts", "virtualFileSystem.ts", @@ -423,6 +425,7 @@ var buildProtocolTs = path.join(scriptsDirectory, "buildProtocol.ts"); var buildProtocolJs = path.join(scriptsDirectory, "buildProtocol.js"); var buildProtocolDts = path.join(builtLocalDirectory, "protocol.d.ts"); var typescriptServicesDts = path.join(builtLocalDirectory, "typescriptServices.d.ts"); +var typesMapJson = path.join(builtLocalDirectory, "typesMap.json"); file(buildProtocolTs); @@ -587,6 +590,16 @@ var serverFile = path.join(builtLocalDirectory, "tsserver.js"); compileFile(serverFile, serverSources, [builtLocalDirectory, copyright, cancellationTokenFile, typingsInstallerFile, watchGuardFile].concat(serverSources).concat(servicesSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { types: ["node"], preserveConstEnums: true, lib: "es6" }); var tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js"); var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts"); +file(typesMapOutputPath, function() { + var content = fs.readFileSync(path.join(serverDirectory, 'typesMap.json')); + // Validate that it's valid JSON + try { + JSON.parse(content.toString()); + } catch (e) { + console.log("Parse error in typesMap.json: " + e); + } + fs.writeFileSync(typesMapOutputPath, content); +}); compileFile( tsserverLibraryFile, languageServiceLibrarySources, @@ -609,7 +622,7 @@ compileFile( // Local target to build the language service server library desc("Builds language service server library"); -task("lssl", [tsserverLibraryFile, tsserverLibraryDefinitionFile]); +task("lssl", [tsserverLibraryFile, tsserverLibraryDefinitionFile, typesMapOutputPath]); desc("Emit the start of the build fold"); task("build-fold-start", [], function () { diff --git a/src/compiler/core.ts b/src/compiler/core.ts index af43b3e59bc..6b94a9eb03d 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -66,13 +66,13 @@ namespace ts { } // The global Map object. This may not be available, so we must test for it. - declare const Map: { new(): Map } | undefined; + declare const Map: { new (): Map } | undefined; // Internet Explorer's Map doesn't support iteration, so don't use it. // tslint:disable-next-line:no-in-operator const MapCtr = typeof Map !== "undefined" && "entries" in Map.prototype ? Map : shimMap(); // Keep the class inside a function so it doesn't get compiled if it's not used. - function shimMap(): { new(): Map } { + function shimMap(): { new (): Map } { class MapIterator { private data: MapLike; @@ -95,7 +95,7 @@ namespace ts { } } - return class implements Map { + return class implements Map { private data = createDictionaryObject(); public size = 0; @@ -158,8 +158,8 @@ namespace ts { } export const enum Comparison { - LessThan = -1, - EqualTo = 0, + LessThan = -1, + EqualTo = 0, GreaterThan = 1 } @@ -2314,6 +2314,17 @@ namespace ts { return (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 (kind: TKind, pos?: number, end?: number) => Token; @@ -2512,8 +2523,7 @@ namespace ts { return findBestPatternMatch(patterns, _ => _, candidate); } - /* @internal */ - export function patternText({prefix, suffix}: Pattern): string { + export function patternText({ prefix, suffix }: Pattern): string { return `${prefix}*${suffix}`; } @@ -2545,7 +2555,7 @@ namespace ts { return matchedValue; } - function isPatternMatch({prefix, suffix}: Pattern, candidate: string) { + function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { return candidate.length >= prefix.length + suffix.length && startsWith(candidate, prefix) && endsWith(candidate, suffix); diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 18109cfa9db..bc4fceac537 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -16,8 +16,8 @@ namespace ts.server { directoryExists: () => false, getDirectories: () => [], createDirectory: noop, - getExecutingFilePath(): string { return void 0; }, - getCurrentDirectory(): string { return void 0; }, + getExecutingFilePath(): string { return ""; }, + getCurrentDirectory(): string { return ""; }, getEnvironmentVariable(): string { return ""; }, readDirectory() { return []; }, exit: noop, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 681a93dcefd..0a56e66abe2 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -28,6 +28,30 @@ namespace ts.projectSystem { }) }; + export const customTypesMap = { + path: "/typesMap.json", + content: `{ + "typesMap": { + "jquery": { + "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", + "types": ["jquery"] + }, + "quack": { + "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js", + "types": ["duck-types"] + } + }, + "simpleMap": { + "Bacon": "baconjs", + "bliss": "blissfuljs", + "commander": "commander", + "cordova": "cordova", + "react": "react", + "lodash": "lodash" + } + }` + }; + export interface PostExecAction { readonly success: boolean; readonly callback: TI.RequestCompletedAction; @@ -204,7 +228,7 @@ namespace ts.projectSystem { byteLength: Utils.byteLength, hrtime: process.hrtime, logger: nullLogger, - canUseEvents: false + canUseEvents: false, }; return new TestSession({ ...sessionOptions, ...opts }); @@ -230,6 +254,7 @@ namespace ts.projectSystem { useInferredProjectPerProjectRoot: false, typingsInstaller, eventHandler, + typesMapLocation: customTypesMap.path, ...opts }); } @@ -1484,7 +1509,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 +1531,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 = { @@ -1542,6 +1567,42 @@ namespace ts.projectSystem { } }); + it("removes version numbers correctly", () => { + const testData: [string, string][] = [ + ["jquery-max", "jquery-max"], + ["jquery.min", "jquery"], + ["jquery-min.4.2.3", "jquery"], + ["jquery.min.4.2.1", "jquery"], + ["minimum", "minimum"], + ["min", "min"], + ["min.3.2", "min"], + ["jquery", "jquery"] + ]; + for (const t of testData) { + assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); + } + }); + + it("ignores files excluded by a legacy safe type list", () => { + const file1 = { + path: "/a/b/bliss.js", + content: "let x = 5" + }; + const file2 = { + path: "/a/b/foo.js", + content: "" + }; + const host = createServerHost([file1, file2, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(), [file2.path]); + } finally { + projectService.resetSafeList(); + } + }); + it("open file become a part of configured project if it is referenced from root file", () => { const file1 = { path: "/a/b/f1.ts", diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 6a6978254c1..69583b60359 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -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, customTypesMap]); 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,36 @@ 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.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) { - enqueueIsCalled = true; super.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports); } installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { @@ -387,18 +383,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) { + 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 +446,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 +485,7 @@ namespace ts.projectSystem { content: "declare const moment: { x: number }" }; - const host = createServerHost([file1, file2, file3, packageJson]); + const host = createServerHost([lodashJs, commanderJs, file3dts, packageJson, customTypesMap]); const installer = new (class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery", "commander", "moment", "express") }); @@ -462,18 +502,24 @@ 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); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + // Commander: Existed as a JS file + // JQuery: Specified in 'include' + // Moment: Specified in 'include' + // lodash: Excluded (not present) + checkProjectActualFiles(p, [file3dts.path, commander.path, jquery.path, moment.path]); }); it("Throttle - delayed typings to install", () => { @@ -521,7 +567,7 @@ namespace ts.projectSystem { }; const typingFiles = [commander, express, jquery, moment, lodash]; - const host = createServerHost([lodashJs, commanderJs, file3, packageJson]); + const host = createServerHost([lodashJs, commanderJs, file3, packageJson, customTypesMap]); const installer = new (class extends Installer { constructor() { super(host, { throttleLimit: 3, typesRegistry: createTypesRegistry("commander", "express", "jquery", "moment", "lodash") }); @@ -543,7 +589,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 @@ -552,7 +598,7 @@ namespace ts.projectSystem { } checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(p, [lodashJs.path, commanderJs.path, 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", () => { @@ -600,7 +646,7 @@ namespace ts.projectSystem { typings: typingsName("gulp") }; - const host = createServerHost([lodashJs, commanderJs, file3]); + const host = createServerHost([lodashJs, commanderJs, file3, customTypesMap]); const installer = new (class extends Installer { constructor() { super(host, { throttleLimit: 1, typesRegistry: createTypesRegistry("commander", "jquery", "lodash", "cordova", "gulp", "grunt") }); @@ -643,7 +689,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(); @@ -653,8 +699,8 @@ 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.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(p1, [file3.path, commander.path, jquery.path, lodash.path, cordova.path]); checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]); }); @@ -1050,11 +1096,12 @@ namespace ts.projectSystem { const host = createServerHost([app, jquery, chroma]); const logger = trackingLogger(); const result = JsTyping.discoverTypings(host, logger.log, [app.path, jquery.path, chroma.path], getDirectoryPath(app.path), safeList, emptyMap, { enable: true }, emptyArray); - assert.deepEqual(logger.finish(), [ + const finish = logger.finish(); + assert.deepEqual(finish, [ 'Inferred typings from file names: ["jquery","chroma-js"]', "Inferred typings from unresolved imports: []", 'Result: {"cachedTypingPaths":[],"newTypingNames":["jquery","chroma-js"],"filesToWatch":["/a/b/bower_components","/a/b/node_modules"]}', - ]); + ], finish.join("\r\n")); assert.deepEqual(result.newTypingNames, ["jquery", "chroma-js"]); }); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts new file mode 100644 index 00000000000..158e460780c --- /dev/null +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -0,0 +1,680 @@ +/// + +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: "/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, 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, 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((s).entries); + } + + function isFile(s: FSEntry): s is File { + return s && isString((s).content); + } + + function invokeWatcherCallbacks(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(map: Map, expectedKeys: ReadonlyArray) { + if (map.size === expectedKeys.length) { + return ""; + } + const notInActual: string[] = []; + const duplicates: string[] = []; + const seen = createMap(); + 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, expectedKeys: ReadonlyArray) { + 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, expectedKeys: ReadonlyArray) { + 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, 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) { + const mapExpected = arrayToSet(expected); + const mapSeen = createMap(); + 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) { + 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 = createMap(); + getCanonicalFileName: (s: string) => string; + private toPath: (f: string) => Path; + private timeoutCallbacks = new Callbacks(); + private immediateCallbacks = new Callbacks(); + + readonly watchedDirectories = createMultiMap(); + readonly watchedDirectoriesRecursive = createMultiMap(); + readonly watchedFiles = createMultiMap(); + private readonly executingFilePath: string; + private readonly currentDirectory: string; + + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, 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, options?: Partial) { + const mapNewLeaves = createMap(); + const isNewFs = this.fs.size === 0; + fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []); + const filesOrFoldersToLoad: ReadonlyArray = !this.useWindowsStylePath ? fileOrFolderList : + fileOrFolderList.map(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, exclude?: ReadonlyArray, include?: ReadonlyArray, 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 { + 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; + } +} diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 59cdfcc6178..c00e1b3cd76 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -109,6 +109,11 @@ namespace ts.server { "smart": IndentStyle.Smart }); + export interface TypesMapFile { + typesMap: SafeList; + simpleMap: { [libName: string]: string }; + } + /** * How to understand this block: * * The 'match' property is a regexp that matches a filename. @@ -345,6 +350,7 @@ namespace ts.server { globalPlugins?: ReadonlyArray; pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; + typesMapLocation?: string; } export class ProjectService { @@ -387,6 +393,7 @@ namespace ts.server { private readonly hostConfiguration: HostConfiguration; private safelist: SafeList = defaultTypeSafeList; + private legacySafelist: { [key: string]: string } = {}; private changedFiles: ScriptInfo[]; @@ -406,6 +413,7 @@ namespace ts.server { public readonly globalPlugins: ReadonlyArray; public readonly pluginProbeLocations: ReadonlyArray; public readonly allowLocalPluginLoads: boolean; + public readonly typesMapLocation: string | undefined; /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects = createMap(); @@ -422,6 +430,7 @@ namespace ts.server { this.globalPlugins = opts.globalPlugins || emptyArray; this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray; this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads; + this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(this.getExecutingFilePath(), "../typesMap.json") : opts.typesMapLocation; Debug.assert(!!this.host.createHash, "'ServerHost.createHash' is required for ProjectService"); @@ -429,6 +438,13 @@ namespace ts.server { this.directoryWatchers = new DirectoryWatchers(this); this.throttledOperations = new ThrottledOperations(this.host); + if (this.typesMapLocation) { + this.loadTypesMap(); + } + else { + this.logger.info("No types map provided; using the default"); + } + this.typingsInstaller.attach(this); this.typingsCache = new TypingsCache(this.typingsInstaller); @@ -466,6 +482,39 @@ namespace ts.server { this.eventHandler(event); } + /*@internal*/ + getNormalizedAbsolutePath(fileName: string) { + return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory()); + } + + /*@internal*/ + getExecutingFilePath() { + return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath()); + } + + 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 = {}; + } + } + updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void { const project = this.findProject(response.projectName); if (!project) { @@ -1085,7 +1134,7 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) { + private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition, excludedFiles: NormalizedPath[]) { const compilerOptions = convertCompilerOptions(options); const project = new ExternalProject( projectFileName, @@ -1094,6 +1143,7 @@ namespace ts.server { compilerOptions, /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader), options.compileOnSave === undefined ? true : options.compileOnSave); + project.excludedFiles = excludedFiles; this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined); this.externalProjects.push(project); @@ -1737,25 +1787,33 @@ 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)); + const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName)) as NormalizedPath[]; for (const name of Object.keys(this.safelist)) { const rule = this.safelist[name]; for (const root of normalizedNames) { if (rule.match.test(root)) { - this.logger.info(`Excluding files based on rule ${name}`); + this.logger.info(`Excluding files based on rule ${name} matching file '${root}'`); // 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); } } } @@ -1793,16 +1851,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 { @@ -1812,8 +1896,14 @@ 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); + const excludedFiles = this.applySafeList(proj); let tsConfigFiles: NormalizedPath[]; const rootFiles: protocol.ExternalFile[] = []; @@ -1906,7 +1996,7 @@ namespace ts.server { else { // no config files - remove the item from the collection this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName); - this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); + this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles); } if (!suppressRefreshOfInferredProjects) { this.refreshInferredProjects(); diff --git a/src/server/project.ts b/src/server/project.ts index 0bb93284ce2..9451e6c2454 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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; @@ -1185,6 +1190,7 @@ namespace ts.server { * These are created only if a host explicitly calls `openExternalProject`. */ export class ExternalProject extends Project { + excludedFiles: ReadonlyArray = []; private typeAcquisition: TypeAcquisition; constructor(public externalProjectName: string, projectService: ProjectService, @@ -1216,26 +1222,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; } } diff --git a/src/server/typesMap.json b/src/server/typesMap.json new file mode 100644 index 00000000000..5e7df046dbe --- /dev/null +++ b/src/server/typesMap.json @@ -0,0 +1,487 @@ +{ + "typesMap": { + "jquery": { + "match": "jquery(-(\\.?\\d+)+)?(\\.intellisense)?(\\.min)?\\.js$", + "types": ["jquery"] + }, + "WinJS": { + "match": "^(.*\\/winjs-[.\\d]+)\\/js\\/base\\.js$", + "exclude": [["^", 1, "/.*"]], + "types": ["winjs"] + }, + "Kendo": { + "match": "^(.*\\/kendo)\\/kendo\\.all\\.min\\.js$", + "exclude": [["^", 1, "/.*"]], + "types": ["kendo-ui"] + }, + "Office Nuget": { + "match": "^(.*\\/office\\/1)\\/excel-\\d+\\.debug\\.js$", + "exclude": [["^", 1, "/.*"]], + "types": ["office"] + }, + "Minified files": { + "match": "^(.+\\.min\\.js)$", + "exclude": [["^", 1, "$"]] + } + }, + "simpleMap": { + "accounting": "accounting", + "ace.js": "ace", + "ag-grid": "ag-grid", + "alertify": "alertify", + "alt": "alt", + "amcharts.js": "amcharts", + "amplify": "amplifyjs", + "angular": "angular", + "angular-bootstrap-lightbox": "angular-bootstrap-lightbox", + "angular-cookie": "angular-cookie", + "angular-file-upload": "angular-file-upload", + "angularfire": "angularfire", + "angular-gettext": "angular-gettext", + "angular-google-analytics": "angular-google-analytics", + "angular-local-storage": "angular-local-storage", + "angularLocalStorage": "angularLocalStorage", + "angular-scroll": "angular-scroll", + "angular-spinner": "angular-spinner", + "angular-strap": "angular-strap", + "angulartics": "angulartics", + "angular-toastr": "angular-toastr", + "angular-translate": "angular-translate", + "angular-ui-router": "angular-ui-router", + "angular-ui-tree": "angular-ui-tree", + "angular-wizard": "angular-wizard", + "async": "async", + "atmosphere": "atmosphere", + "aws-sdk": "aws-sdk", + "aws-sdk-js": "aws-sdk", + "axios": "axios", + "backbone": "backbone", + "backbone.layoutmanager": "backbone.layoutmanager", + "backbone.paginator": "backbone.paginator", + "backbone.radio": "backbone.radio", + "backbone-associations": "backbone-associations", + "backbone-relational": "backbone-relational", + "backgrid": "backgrid", + "Bacon": "baconjs", + "benchmark": "benchmark", + "blazy": "blazy", + "bliss": "blissfuljs", + "bluebird": "bluebird", + "body-parser": "body-parser", + "bootbox": "bootbox", + "bootstrap": "bootstrap", + "bootstrap-editable": "x-editable", + "bootstrap-maxlength": "bootstrap-maxlength", + "bootstrap-notify": "bootstrap-notify", + "bootstrap-slider": "bootstrap-slider", + "bootstrap-switch": "bootstrap-switch", + "bowser": "bowser", + "breeze": "breeze", + "browserify": "browserify", + "bson": "bson", + "c3": "c3", + "canvasjs": "canvasjs", + "chai": "chai", + "chalk": "chalk", + "chance": "chance", + "chartist": "chartist", + "cheerio": "cheerio", + "chokidar": "chokidar", + "chosen.jquery": "chosen", + "chroma": "chroma-js", + "ckeditor.js": "ckeditor", + "cli-color": "cli-color", + "clipboard": "clipboard", + "codemirror": "codemirror", + "colors": "colors", + "commander": "commander", + "commonmark": "commonmark", + "compression": "compression", + "confidence": "confidence", + "connect": "connect", + "Control.FullScreen": "leaflet.fullscreen", + "cookie": "cookie", + "cookie-parser": "cookie-parser", + "cookies": "cookies", + "core": "core-js", + "core-js": "core-js", + "crossfilter": "crossfilter", + "crossroads": "crossroads", + "css": "css", + "ct-ui-router-extras": "ui-router-extras", + "d3": "d3", + "dagre-d3": "dagre-d3", + "dat.gui": "dat-gui", + "debug": "debug", + "deep-diff": "deep-diff", + "Dexie": "dexie", + "dialogs": "angular-dialog-service", + "dojo.js": "dojo", + "doT": "dot", + "dragula": "dragula", + "drop": "drop", + "dropbox": "dropboxjs", + "dropzone": "dropzone", + "Dts Name": "Dts Name", + "dust-core": "dustjs-linkedin", + "easeljs": "easeljs", + "ejs": "ejs", + "ember": "ember", + "envify": "envify", + "epiceditor": "epiceditor", + "es6-promise": "es6-promise", + "ES6-Promise": "es6-promise", + "es6-shim": "es6-shim", + "expect": "expect", + "express": "express", + "express-session": "express-session", + "ext-all.js": "extjs", + "extend": "extend", + "fabric": "fabricjs", + "faker": "faker", + "fastclick": "fastclick", + "favico": "favico.js", + "featherlight": "featherlight", + "FileSaver": "FileSaver", + "fingerprint": "fingerprintjs", + "fixed-data-table": "fixed-data-table", + "flickity.pkgd": "flickity", + "flight": "flight", + "flow": "flowjs", + "Flux": "flux", + "formly": "angular-formly", + "foundation": "foundation", + "fpsmeter": "fpsmeter", + "fuse": "fuse", + "generator": "yeoman-generator", + "gl-matrix": "gl-matrix", + "globalize": "globalize", + "graceful-fs": "graceful-fs", + "gridstack": "gridstack", + "gulp": "gulp", + "gulp-rename": "gulp-rename", + "gulp-uglify": "gulp-uglify", + "gulp-util": "gulp-util", + "hammer": "hammerjs", + "handlebars": "handlebars", + "hasher": "hasher", + "he": "he", + "hello.all": "hellojs", + "highcharts.js": "highcharts", + "highlight": "highlightjs", + "history": "history", + "History": "history", + "hopscotch": "hopscotch", + "hotkeys": "angular-hotkeys", + "html2canvas": "html2canvas", + "humane": "humane", + "i18next": "i18next", + "icheck": "icheck", + "impress": "impress", + "incremental-dom": "incremental-dom", + "Inquirer": "inquirer", + "insight": "insight", + "interact": "interactjs", + "intercom": "intercomjs", + "intro": "intro.js", + "ion.rangeSlider": "ion.rangeSlider", + "ionic": "ionic", + "is": "is_js", + "iscroll": "iscroll", + "jade": "jade", + "jasmine": "jasmine", + "joint": "jointjs", + "jquery": "jquery", + "jquery.address": "jquery.address", + "jquery.are-you-sure": "jquery.are-you-sure", + "jquery.blockUI": "jquery.blockUI", + "jquery.bootstrap.wizard": "jquery.bootstrap.wizard", + "jquery.bootstrap-touchspin": "bootstrap-touchspin", + "jquery.color": "jquery.color", + "jquery.colorbox": "jquery.colorbox", + "jquery.contextMenu": "jquery.contextMenu", + "jquery.cookie": "jquery.cookie", + "jquery.customSelect": "jquery.customSelect", + "jquery.cycle.all": "jquery.cycle", + "jquery.cycle2": "jquery.cycle2", + "jquery.dataTables": "jquery.dataTables", + "jquery.dropotron": "jquery.dropotron", + "jquery.fancybox.pack.js": "fancybox", + "jquery.fancytree-all": "jquery.fancytree", + "jquery.fileupload": "jquery.fileupload", + "jquery.flot": "flot", + "jquery.form": "jquery.form", + "jquery.gridster": "jquery.gridster", + "jquery.handsontable.full": "jquery-handsontable", + "jquery.joyride": "jquery.joyride", + "jquery.jqGrid": "jqgrid", + "jquery.mmenu": "jquery.mmenu", + "jquery.mockjax": "jquery-mockjax", + "jquery.noty": "jquery.noty", + "jquery.payment": "jquery.payment", + "jquery.pjax": "jquery.pjax", + "jquery.placeholder": "jquery.placeholder", + "jquery.qrcode": "jquery.qrcode", + "jquery.qtip": "qtip2", + "jquery.raty": "raty", + "jquery.scrollTo": "jquery.scrollTo", + "jquery.signalR": "signalr", + "jquery.simplemodal": "jquery.simplemodal", + "jquery.timeago": "jquery.timeago", + "jquery.tinyscrollbar": "jquery.tinyscrollbar", + "jquery.tipsy": "jquery.tipsy", + "jquery.tooltipster": "tooltipster", + "jquery.transit": "jquery.transit", + "jquery.uniform": "jquery.uniform", + "jquery.watch": "watch", + "jquery-sortable": "jquery-sortable", + "jquery-ui": "jqueryui", + "js.cookie": "js-cookie", + "js-data": "js-data", + "js-data-angular": "js-data-angular", + "js-data-http": "js-data-http", + "jsdom": "jsdom", + "jsnlog": "jsnlog", + "json5": "json5", + "jspdf": "jspdf", + "jsrender": "jsrender", + "js-signals": "js-signals", + "jstorage": "jstorage", + "jstree": "jstree", + "js-yaml": "js-yaml", + "jszip": "jszip", + "katex": "katex", + "kefir": "kefir", + "keymaster": "keymaster", + "keypress": "keypress", + "kinetic": "kineticjs", + "knockback": "knockback", + "knockout": "knockout", + "knockout.mapping": "knockout.mapping", + "knockout.validation": "knockout.validation", + "knockout-paging": "knockout-paging", + "knockout-pre-rendered": "knockout-pre-rendered", + "ladda": "ladda", + "later": "later", + "lazy": "lazy.js", + "Leaflet.Editable": "leaflet-editable", + "leaflet.js": "leaflet", + "less": "less", + "linq": "linq", + "loading-bar": "angular-loading-bar", + "lodash": "lodash", + "log4javascript": "log4javascript", + "loglevel": "loglevel", + "lokijs": "lokijs", + "lovefield": "lovefield", + "lunr": "lunr", + "lz-string": "lz-string", + "mailcheck": "mailcheck", + "maquette": "maquette", + "marked": "marked", + "math": "mathjs", + "MathJax.js": "mathjax", + "matter": "matter-js", + "md5": "blueimp-md5", + "md5.js": "crypto-js", + "messenger": "messenger", + "method-override": "method-override", + "minimatch": "minimatch", + "minimist": "minimist", + "mithril": "mithril", + "mobile-detect": "mobile-detect", + "mocha": "mocha", + "mock-ajax": "jasmine-ajax", + "modernizr": "modernizr", + "Modernizr": "Modernizr", + "moment": "moment", + "moment-range": "moment-range", + "moment-timezone": "moment-timezone", + "mongoose": "mongoose", + "morgan": "morgan", + "mousetrap": "mousetrap", + "ms": "ms", + "mustache": "mustache", + "native.history": "history", + "nconf": "nconf", + "ncp": "ncp", + "nedb": "nedb", + "ng-cordova": "ng-cordova", + "ngDialog": "ng-dialog", + "ng-flow-standalone": "ng-flow", + "ng-grid": "ng-grid", + "ng-i18next": "ng-i18next", + "ng-table": "ng-table", + "node_redis": "redis", + "node-clone": "clone", + "node-fs-extra": "fs-extra", + "node-glob": "glob", + "Nodemailer": "nodemailer", + "node-mime": "mime", + "node-mkdirp": "mkdirp", + "node-mongodb-native": "mongodb", + "node-mysql": "mysql", + "node-open": "open", + "node-optimist": "optimist", + "node-progress": "progress", + "node-semver": "semver", + "node-tar": "tar", + "node-uuid": "node-uuid", + "node-xml2js": "xml2js", + "nopt": "nopt", + "notify": "notify", + "nouislider": "nouislider", + "npm": "npm", + "nprogress": "nprogress", + "numbro": "numbro", + "numeral": "numeraljs", + "nunjucks": "nunjucks", + "nv.d3": "nvd3", + "object-assign": "object-assign", + "oboe-browser": "oboe", + "office": "office-js", + "offline": "offline-js", + "onsenui": "onsenui", + "OpenLayers.js": "openlayers", + "openpgp": "openpgp", + "p2": "p2", + "packery.pkgd": "packery", + "page": "page", + "pako": "pako", + "papaparse": "papaparse", + "passport": "passport", + "passport-local": "passport-local", + "path": "pathjs", + "peer": "peerjs", + "peg": "pegjs", + "photoswipe": "photoswipe", + "picker.js": "pickadate", + "pikaday": "pikaday", + "pixi": "pixi.js", + "platform": "platform", + "Please": "pleasejs", + "plottable": "plottable", + "polymer": "polymer", + "postal": "postal", + "preloadjs": "preloadjs", + "progress": "progress", + "purify": "dompurify", + "purl": "purl", + "q": "q", + "qs": "qs", + "qunit": "qunit", + "ractive": "ractive", + "rangy-core": "rangy", + "raphael": "raphael", + "raven": "ravenjs", + "react": "react", + "react-bootstrap": "react-bootstrap", + "react-intl": "react-intl", + "react-redux": "react-redux", + "ReactRouter": "react-router", + "ready": "domready", + "redux": "redux", + "request": "request", + "require": "require", + "restangular": "restangular", + "reveal": "reveal", + "rickshaw": "rickshaw", + "rimraf": "rimraf", + "rivets": "rivets", + "rx": "rx", + "rx.angular": "rx-angular", + "sammy": "sammyjs", + "SAT": "sat", + "sax-js": "sax", + "screenfull": "screenfull", + "seedrandom": "seedrandom", + "select2": "select2", + "selectize": "selectize", + "serve-favicon": "serve-favicon", + "serve-static": "serve-static", + "shelljs": "shelljs", + "should": "should", + "showdown": "showdown", + "sigma": "sigmajs", + "signature_pad": "signature_pad", + "sinon": "sinon", + "sjcl": "sjcl", + "slick": "slick-carousel", + "smoothie": "smoothie", + "socket.io": "socket.io", + "socket.io-client": "socket.io-client", + "sockjs": "sockjs-client", + "sortable": "angular-ui-sortable", + "soundjs": "soundjs", + "source-map": "source-map", + "spectrum": "spectrum", + "spin": "spin", + "sprintf": "sprintf", + "stampit": "stampit", + "state-machine": "state-machine", + "Stats": "stats", + "store": "storejs", + "string": "string", + "string_score": "string_score", + "strophe": "strophe", + "stylus": "stylus", + "sugar": "sugar", + "superagent": "superagent", + "svg": "svgjs", + "svg-injector": "svg-injector", + "swfobject": "swfobject", + "swig": "swig", + "swipe": "swipe", + "swiper": "swiper", + "system.js": "systemjs", + "tether": "tether", + "three": "threejs", + "through": "through", + "through2": "through2", + "timeline": "timelinejs", + "tinycolor": "tinycolor", + "tmhDynamicLocale": "angular-dynamic-locale", + "toaster": "angularjs-toaster", + "toastr": "toastr", + "tracking": "tracking", + "trunk8": "trunk8", + "turf": "turf", + "tweenjs": "tweenjs", + "TweenMax": "gsap", + "twig": "twig", + "twix": "twix", + "typeahead.bundle": "typeahead", + "typescript": "typescript", + "ui": "winjs", + "ui-bootstrap-tpls": "angular-ui-bootstrap", + "ui-grid": "ui-grid", + "uikit": "uikit", + "underscore": "underscore", + "underscore.string": "underscore.string", + "update-notifier": "update-notifier", + "url": "jsurl", + "UUID": "uuid", + "validator": "validator", + "vega": "vega", + "vex": "vex-js", + "video": "videojs", + "vue": "vue", + "vue-router": "vue-router", + "webtorrent": "webtorrent", + "when": "when", + "winston": "winston", + "wrench-js": "wrench", + "ws": "ws", + "xlsx": "xlsx", + "xml2json": "x2js", + "xmlbuilder-js": "xmlbuilder", + "xregexp": "xregexp", + "yargs": "yargs", + "yosay": "yosay", + "yui": "yui", + "yui3": "yui", + "zepto": "zepto", + "ZeroClipboard": "zeroclipboard", + "ZSchema-browser": "z-schema" + } +} \ No newline at end of file diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index f005ec7de8f..a9b28ce57b0 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -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`); } diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index 5e7b7d424f8..7d451f5de52 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -172,7 +172,7 @@ namespace ts.JsTyping { if (!hasJavaScriptFileExtension(j)) return undefined; const inferredTypingName = removeFileExtension(getBaseFileName(j.toLowerCase())); - const cleanedTypingName = inferredTypingName.replace(/((?:\.|-)min(?=\.|$))|((?:-|\.)\d+)/g, ""); + const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName); return safeList.get(cleanedTypingName); }); if (fromFileNames.length) {