diff --git a/src/project-manager.ts b/src/project-manager.ts index a5ef62198..d25327d6e 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -74,6 +74,11 @@ export class ProjectManager implements Disposable { */ private ensuredModuleStructure?: Observable; + /** + * Observable that completes when extra dependencies pointed to by tsconfig.json have been loaded. + */ + private ensuredConfigDependencies?: Observable; + /** * Observable that completes when `ensureAllFiles` completed */ @@ -252,6 +257,7 @@ export class ProjectManager implements Disposable { */ invalidateModuleStructure(): void { this.ensuredModuleStructure = undefined; + this.ensuredConfigDependencies = undefined; this.ensuredAllFiles = undefined; this.ensuredOwnFiles = undefined; } @@ -321,6 +327,7 @@ export class ProjectManager implements Disposable { span.addTags({ uri, maxDepth }); ignore.add(uri); return this.ensureModuleStructure(span) + .concat(Observable.defer(() => this.ensureConfigDependencies())) // If max depth was reached, don't go any further .concat(Observable.defer(() => maxDepth === 0 ? Observable.empty() : this.resolveReferencedFiles(uri))) // Prevent cycles @@ -337,6 +344,39 @@ export class ProjectManager implements Disposable { }); } + /** + * Determines if a tsconfig/jsconfig needs additional declaration files loaded. + * @param filePath + */ + isConfigDependency(filePath: string): boolean { + for (const config of this.configurations()) { + config.ensureConfigFile(); + if (config.isExpectedDeclarationFile(filePath)) { + return true; + } + } + return false; + } + + /** + * Loads files determined by tsconfig to be needed into the file system + */ + ensureConfigDependencies(childOf = new Span()): Observable { + return traceObservable('Ensure config dependencies', childOf, span => { + if (!this.ensuredConfigDependencies) { + this.ensuredConfigDependencies = observableFromIterable(this.inMemoryFs.uris()) + .filter(uri => this.isConfigDependency(uri2path(uri))) + .mergeMap(uri => this.updater.ensure(uri)) + .do(noop, err => { + this.ensuredConfigDependencies = undefined; + }) + .publishReplay() + .refCount() as Observable; + } + return this.ensuredConfigDependencies; + }); + } + /** * Invalidates a cache entry for `resolveReferencedFiles` (e.g. because the file changed) * @@ -734,6 +774,11 @@ export class ProjectConfiguration { */ private expectedFilePaths = new Set(); + /** + * List of resolved extra root directories to allow global type declaration files to be loaded from. + */ + private typeRoots: string[]; + /** * @param fs file system to use * @param documentRegistry Shared DocumentRegistry that manages SourceFile objects @@ -838,6 +883,11 @@ export class ProjectConfiguration { this.expectedFilePaths = new Set(configParseResult.fileNames); const options = configParseResult.options; + const pathResolver = /^[a-z]:\//i.test(base) ? path.win32 : path.posix; + this.typeRoots = options.typeRoots ? + options.typeRoots.map((r: string) => pathResolver.resolve(this.rootFilePath, r)) : + []; + if (/(^|\/)jsconfig\.json$/.test(this.configFilePath)) { options.allowJs = true; } @@ -864,6 +914,16 @@ export class ProjectConfiguration { private ensuredBasicFiles = false; + /** + * Determines if a fileName is a declaration file within expected files or type roots + * @param fileName + */ + public isExpectedDeclarationFile(fileName: string) { + return isDeclarationFile(fileName) && + (this.expectedFilePaths.has(toUnixPath(fileName)) || + this.typeRoots.some(root => fileName.startsWith(root))); + } + /** * Ensures we added basic files (global TS files, dependencies, declarations) */ @@ -882,7 +942,8 @@ export class ProjectConfiguration { // Add all global declaration files from the workspace and all declarations from the project for (const uri of this.fs.uris()) { const fileName = uri2path(uri); - if (isGlobalTSFile(fileName) || (isDeclarationFile(fileName) && this.expectedFilePaths.has(toUnixPath(fileName)))) { + if (isGlobalTSFile(fileName) || + this.isExpectedDeclarationFile(fileName)) { const sourceFile = program.getSourceFile(fileName); if (!sourceFile) { this.getHost().addFile(fileName); diff --git a/src/test/project-manager.test.ts b/src/test/project-manager.test.ts index ee40fabfe..aa8e8392b 100644 --- a/src/test/project-manager.test.ts +++ b/src/test/project-manager.test.ts @@ -24,6 +24,26 @@ describe('ProjectManager', () => { assert.isDefined(configs.find(config => config.configFilePath === '/foo/tsconfig.json')); }); + describe('ensureBasicFiles', () => { + beforeEach(async () => { + memfs = new InMemoryFileSystem('/'); + const localfs = new MapFileSystem(new Map([ + ['file:///project/package.json', '{"name": "package-name-1"}'], + ['file:///project/tsconfig.json', '{ "compilerOptions": { "typeRoots": ["../types"]} }'], + ['file:///project/file.ts', 'console.log(GLOBALCONSTANT);'], + ['file:///types/types.d.ts', 'declare var GLOBALCONSTANT=1;'] + + ])); + const updater = new FileSystemUpdater(localfs, memfs); + projectManager = new ProjectManager('/', memfs, updater, true); + }); + it('loads files from typeRoots', async () => { + await projectManager.ensureReferencedFiles('file:///project/file.ts').toPromise(); + memfs.getContent('file:///project/file.ts'); + memfs.getContent('file:///types/types.d.ts'); + }); + }); + describe('getPackageName()', () => { beforeEach(async () => { memfs = new InMemoryFileSystem('/');