diff --git a/src/project-manager.ts b/src/project-manager.ts index 8de21e064..faf0a858b 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -24,6 +24,8 @@ import { uri2path } from './util' +const LAST_FORWARD_OR_BACKWARD_SLASH = /[\\\/][^\\\/]*$/ + /** * Implementaton of LanguageServiceHost that works with in-memory file system. * It takes file content from local cache and provides it to TS compiler on demand @@ -183,6 +185,10 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { * made available to the compiler before calling any other methods on * the ProjectConfiguration or its public members. By default, no * files are parsed. + * + * Windows file paths are converted to UNIX-style forward slashes + * when compared with Typescript configuration (isGlobalTSFile, + * expectedFilePaths and typeRoots) */ export class ProjectConfiguration { @@ -225,12 +231,13 @@ export class ProjectConfiguration { /** * List of files that project consist of (based on tsconfig includes/excludes and wildcards). - * Each item is a relative file path + * Each item is a relative UNIX-like file path */ private expectedFilePaths = new Set() /** * List of resolved extra root directories to allow global type declaration files to be loaded from. + * Each item is an absolute UNIX-like file path */ private typeRoots: string[] @@ -343,7 +350,7 @@ export class ProjectConfiguration { 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)) : + options.typeRoots.map((r: string) => toUnixPath(pathResolver.resolve(this.rootFilePath, r))) : [] if (/(^|\/)jsconfig\.json$/.test(this.configFilePath)) { @@ -401,11 +408,11 @@ export class ProjectConfiguration { /** * Determines if a fileName is a declaration file within expected files or type roots - * @param fileName + * @param fileName A Unix-like absolute file path. */ public isExpectedDeclarationFile(fileName: string): boolean { return isDeclarationFile(fileName) && - (this.expectedFilePaths.has(toUnixPath(fileName)) || + (this.expectedFilePaths.has(fileName) || this.typeRoots.some(root => fileName.startsWith(root))) } @@ -427,8 +434,9 @@ 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) || - this.isExpectedDeclarationFile(fileName)) { + const unixPath = toUnixPath(fileName) + if (isGlobalTSFile(unixPath) || + this.isExpectedDeclarationFile(unixPath)) { const sourceFile = program.getSourceFile(fileName) if (!sourceFile) { this.getHost().addFile(fileName) @@ -487,6 +495,8 @@ export type ConfigType = 'js' | 'ts' * makes one or more LanguageService objects. By default all LanguageService objects contain no files, * they are added on demand - current file for hover or definition, project's files for references and * all files from all projects for workspace symbols. + * + * ProjectManager preserves Windows paths until passed to ProjectConfiguration or TS APIs. */ export class ProjectManager implements Disposable { @@ -588,7 +598,7 @@ export class ProjectManager implements Disposable { // Create catch-all fallback configs in case there are no tsconfig.json files // They are removed once at least one tsconfig.json is found - const trimmedRootPath = this.rootPath.replace(/\/+$/, '') + const trimmedRootPath = this.rootPath.replace(/[\\\/]+$/, '') const fallbackConfigs: {js?: ProjectConfiguration, ts?: ProjectConfiguration} = {} for (const configType of ['js', 'ts'] as ConfigType[]) { const configs = this.configs[configType] @@ -621,13 +631,8 @@ export class ProjectManager implements Disposable { .filter(([uri, content]) => !!content && /\/[tj]sconfig\.json/.test(uri) && !uri.includes('/node_modules/')) .subscribe(([uri, content]) => { const filePath = uri2path(uri) - let dir = toUnixPath(filePath) - const pos = dir.lastIndexOf('/') - if (pos <= 0) { - dir = '' - } else { - dir = dir.substring(0, pos) - } + const pos = filePath.search(LAST_FORWARD_OR_BACKWARD_SLASH) + const dir = pos <= 0 ? '' : filePath.substring(0, pos) const configType = this.getConfigurationType(filePath) const configs = this.configs[configType] configs.set(dir, new ProjectConfiguration( @@ -813,7 +818,7 @@ export class ProjectManager implements Disposable { /** * Determines if a tsconfig/jsconfig needs additional declaration files loaded. - * @param filePath + * @param filePath A UNIX-like absolute file path */ public isConfigDependency(filePath: string): boolean { for (const config of this.configurations()) { @@ -832,7 +837,7 @@ export class ProjectManager implements Disposable { return traceObservable('Ensure config dependencies', childOf, span => { if (!this.ensuredConfigDependencies) { this.ensuredConfigDependencies = observableFromIterable(this.inMemoryFs.uris()) - .filter(uri => this.isConfigDependency(uri2path(uri))) + .filter(uri => this.isConfigDependency(toUnixPath(uri2path(uri)))) .mergeMap(uri => this.updater.ensure(uri)) .do(noop, err => { this.ensuredConfigDependencies = undefined @@ -929,19 +934,19 @@ export class ProjectManager implements Disposable { * @return closest configuration for a given file path or undefined if there is no such configuration */ public getConfigurationIfExists(filePath: string, configType = this.getConfigurationType(filePath)): ProjectConfiguration | undefined { - let dir = toUnixPath(filePath) + let dir = filePath let config: ProjectConfiguration | undefined const configs = this.configs[configType] if (!configs) { return undefined } - const rootPath = this.rootPath.replace(/\/+$/, '') + const rootPath = this.rootPath.replace(/[\\\/]+$/, '') while (dir && dir !== rootPath) { config = configs.get(dir) if (config) { return config } - const pos = dir.lastIndexOf('/') + const pos = dir.search(LAST_FORWARD_OR_BACKWARD_SLASH) if (pos <= 0) { dir = '' } else { @@ -1029,13 +1034,14 @@ export class ProjectManager implements Disposable { * @return configuration type to use for a given file */ private getConfigurationType(filePath: string): ConfigType { - const name = path.posix.basename(filePath) + const unixPath = toUnixPath(filePath) + const name = path.posix.basename(unixPath) if (name === 'tsconfig.json') { return 'ts' } else if (name === 'jsconfig.json') { return 'js' } - const extension = path.posix.extname(filePath) + const extension = path.posix.extname(unixPath) if (extension === '.js' || extension === '.jsx') { return 'js' } diff --git a/src/test/project-manager.test.ts b/src/test/project-manager.test.ts index db8ea1167..0c067ce56 100644 --- a/src/test/project-manager.test.ts +++ b/src/test/project-manager.test.ts @@ -3,134 +3,173 @@ import chaiAsPromised = require('chai-as-promised') import { FileSystemUpdater } from '../fs' import { InMemoryFileSystem } from '../memfs' import { ProjectManager } from '../project-manager' +import { uri2path } from '../util' import { MapFileSystem } from './fs-helpers' chai.use(chaiAsPromised) const assert = chai.assert describe('ProjectManager', () => { + for (const rootUri of ['file:///', 'file:///c:/foo/bar/', 'file:///foo/bar/']) { + describe(`with rootUri ${rootUri}`, () => { - let projectManager: ProjectManager - let memfs: InMemoryFileSystem + let projectManager: ProjectManager + let memfs: InMemoryFileSystem - it('should add a ProjectConfiguration when a tsconfig.json is added to the InMemoryFileSystem', () => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///foo/tsconfig.json', '{}'] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - memfs.add('file:///foo/tsconfig.json', '{}') - const configs = Array.from(projectManager.configurations()) - assert.isDefined(configs.find(config => config.configFilePath === '/foo/tsconfig.json')) - }) + it('should add a ProjectConfiguration when a tsconfig.json is added to the InMemoryFileSystem', () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const configFileUri = rootUri + 'foo/tsconfig.json' + const localfs = new MapFileSystem(new Map([ + [configFileUri, '{}'] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + memfs.add(configFileUri, '{}') + const configs = Array.from(projectManager.configurations()) + const expectedConfigFilePath = uri2path(configFileUri) - 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;'] + assert.isDefined(configs.find(config => config.configFilePath === expectedConfigFilePath)) + }) - ])) - 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('ensureBasicFiles', () => { + beforeEach(async () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'project/package.json', '{"name": "package-name-1"}'], + [rootUri + 'project/tsconfig.json', '{ "compilerOptions": { "typeRoots": ["../types"]} }'], + [rootUri + 'project/node_modules/%40types/mocha/index.d.ts', 'declare var describe { (description: string, spec: () => void): void; }'], + [rootUri + 'project/file.ts', 'describe("test", () => console.log(GLOBALCONSTANT));'], + [rootUri + 'types/types.d.ts', 'declare var GLOBALCONSTANT=1;'] - describe('getPackageName()', () => { - beforeEach(async () => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///package.json', '{"name": "package-name-1"}'], - ['file:///subdirectory-with-tsconfig/package.json', '{"name": "package-name-2"}'], - ['file:///subdirectory-with-tsconfig/src/tsconfig.json', '{}'], - ['file:///subdirectory-with-tsconfig/src/dummy.ts', ''] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - await projectManager.ensureAllFiles().toPromise() - }) - }) - describe('ensureReferencedFiles()', () => { - beforeEach(() => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///package.json', '{"name": "package-name-1"}'], - ['file:///node_modules/somelib/index.js', '/// \n/// '], - ['file:///node_modules/somelib/pathref.d.ts', ''], - ['file:///node_modules/%40types/node/index.d.ts', ''], - ['file:///src/dummy.ts', 'import * as somelib from "somelib";'] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - }) - it('should ensure content for imports and references is fetched', async () => { - await projectManager.ensureReferencedFiles('file:///src/dummy.ts').toPromise() - memfs.getContent('file:///node_modules/somelib/index.js') - memfs.getContent('file:///node_modules/somelib/pathref.d.ts') - memfs.getContent('file:///node_modules/%40types/node/index.d.ts') - }) - }) - describe('getConfiguration()', () => { - beforeEach(async () => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///tsconfig.json', '{}'], - ['file:///src/jsconfig.json', '{}'] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - await projectManager.ensureAllFiles().toPromise() - }) - it('should resolve best configuration based on file name', () => { - const jsConfig = projectManager.getConfiguration('/src/foo.js') - const tsConfig = projectManager.getConfiguration('/src/foo.ts') - assert.equal('/tsconfig.json', tsConfig.configFilePath) - assert.equal('/src/jsconfig.json', jsConfig.configFilePath) - }) - }) - describe('getParentConfiguration()', () => { - beforeEach(async () => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///tsconfig.json', '{}'], - ['file:///src/jsconfig.json', '{}'] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - await projectManager.ensureAllFiles().toPromise() - }) - it('should resolve best configuration based on file name', () => { - const config = projectManager.getParentConfiguration('file:///src/foo.ts') - assert.isDefined(config) - assert.equal('/tsconfig.json', config!.configFilePath) - }) - }) - describe('getChildConfigurations()', () => { - beforeEach(async () => { - memfs = new InMemoryFileSystem('/') - const localfs = new MapFileSystem(new Map([ - ['file:///tsconfig.json', '{}'], - ['file:///foo/bar/tsconfig.json', '{}'], - ['file:///foo/baz/tsconfig.json', '{}'] - ])) - const updater = new FileSystemUpdater(localfs, memfs) - projectManager = new ProjectManager('/', memfs, updater, true) - await projectManager.ensureAllFiles().toPromise() - }) - it('should resolve best configuration based on file name', () => { - const configs = Array.from(projectManager.getChildConfigurations('file:///foo')).map(config => config.configFilePath) - assert.deepEqual(configs, [ - '/foo/bar/tsconfig.json', - '/foo/baz/tsconfig.json' - ]) + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + }) + + it('loads files from typeRoots', async () => { + const sourceFileUri = rootUri + 'project/file.ts' + const typeRootFileUri = rootUri + 'types/types.d.ts' + await projectManager.ensureReferencedFiles(sourceFileUri).toPromise() + memfs.getContent(typeRootFileUri) + + const config = projectManager.getConfiguration(uri2path(sourceFileUri), 'ts') + const host = config.getHost() + const typeDeclarationPath = uri2path(typeRootFileUri) + assert.includeMembers(host.getScriptFileNames(), [typeDeclarationPath]) + }) + + it('loads mocha global type declarations', async () => { + const sourceFileUri = rootUri + 'project/file.ts' + const mochaDeclarationFileUri = rootUri + 'project/node_modules/%40types/mocha/index.d.ts' + await projectManager.ensureReferencedFiles(sourceFileUri).toPromise() + memfs.getContent(mochaDeclarationFileUri) + + const config = projectManager.getConfiguration(uri2path(sourceFileUri), 'ts') + const host = config.getHost() + const mochaFilePath = uri2path(mochaDeclarationFileUri) + assert.includeMembers(host.getScriptFileNames(), [mochaFilePath]) + }) + }) + + describe('getPackageName()', () => { + beforeEach(async () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'package.json', '{"name": "package-name-1"}'], + [rootUri + 'subdirectory-with-tsconfig/package.json', '{"name": "package-name-2"}'], + [rootUri + 'subdirectory-with-tsconfig/src/tsconfig.json', '{}'], + [rootUri + 'subdirectory-with-tsconfig/src/dummy.ts', ''] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + await projectManager.ensureAllFiles().toPromise() + }) + }) + + describe('ensureReferencedFiles()', () => { + beforeEach(() => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'package.json', '{"name": "package-name-1"}'], + [rootUri + 'node_modules/somelib/index.js', '/// \n/// '], + [rootUri + 'node_modules/somelib/pathref.d.ts', ''], + [rootUri + 'node_modules/%40types/node/index.d.ts', ''], + [rootUri + 'src/dummy.ts', 'import * as somelib from "somelib";'] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + }) + it('should ensure content for imports and references is fetched', async () => { + await projectManager.ensureReferencedFiles(rootUri + 'src/dummy.ts').toPromise() + memfs.getContent(rootUri + 'node_modules/somelib/index.js') + memfs.getContent(rootUri + 'node_modules/somelib/pathref.d.ts') + memfs.getContent(rootUri + 'node_modules/%40types/node/index.d.ts') + }) + }) + describe('getConfiguration()', () => { + beforeEach(async () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'tsconfig.json', '{}'], + [rootUri + 'src/jsconfig.json', '{}'] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + await projectManager.ensureAllFiles().toPromise() + }) + it('should resolve best configuration based on file name', () => { + const jsConfig = projectManager.getConfiguration(uri2path(rootUri + 'src/foo.js')) + const tsConfig = projectManager.getConfiguration(uri2path(rootUri + 'src/foo.ts')) + assert.equal(uri2path(rootUri + 'tsconfig.json'), tsConfig.configFilePath) + assert.equal(uri2path(rootUri + 'src/jsconfig.json'), jsConfig.configFilePath) + assert.equal(Array.from(projectManager.configurations()).length, 2) + }) + }) + describe('getParentConfiguration()', () => { + beforeEach(async () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'tsconfig.json', '{}'], + [rootUri + 'src/jsconfig.json', '{}'] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + await projectManager.ensureAllFiles().toPromise() + }) + it('should resolve best configuration based on file name', () => { + const config = projectManager.getParentConfiguration(rootUri + 'src/foo.ts') + assert.isDefined(config) + assert.equal(uri2path(rootUri + 'tsconfig.json'), config!.configFilePath) + assert.equal(Array.from(projectManager.configurations()).length, 2) + }) + }) + describe('getChildConfigurations()', () => { + beforeEach(async () => { + const rootPath = uri2path(rootUri) + memfs = new InMemoryFileSystem(rootPath) + const localfs = new MapFileSystem(new Map([ + [rootUri + 'tsconfig.json', '{}'], + [rootUri + 'foo/bar/tsconfig.json', '{}'], + [rootUri + 'foo/baz/tsconfig.json', '{}'] + ])) + const updater = new FileSystemUpdater(localfs, memfs) + projectManager = new ProjectManager(rootPath, memfs, updater, true) + await projectManager.ensureAllFiles().toPromise() + }) + it('should resolve best configuration based on file name', () => { + const configs = Array.from(projectManager.getChildConfigurations(rootUri + 'foo')).map(config => config.configFilePath) + assert.deepEqual(configs, [ + uri2path(rootUri + 'foo/bar/tsconfig.json'), + uri2path(rootUri + 'foo/baz/tsconfig.json') + ]) + assert.equal(Array.from(projectManager.configurations()).length, 4) + }) + }) }) - }) + } })