Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.

Fix global type loading on Windows paths #362

Merged
merged 10 commits into from
Oct 9, 2017
Merged
48 changes: 27 additions & 21 deletions src/project-manager.ts
Original file line number Diff line number Diff line change
@@ -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<string>()

/**
* 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'
}
277 changes: 158 additions & 119 deletions src/test/project-manager.test.ts
Original file line number Diff line number Diff line change
@@ -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', '/// <reference path="./pathref.d.ts"/>\n/// <reference types="node"/>'],
['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', '/// <reference path="./pathref.d.ts"/>\n/// <reference types="node"/>'],
[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)
})
})
})
})
}
})